@kamel-ahmed/proxy-claude 1.0.2 → 1.0.4
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/bin/cli.js +316 -37
- package/package.json +2 -2
- package/src/cli/onboard.js +469 -0
package/bin/cli.js
CHANGED
|
@@ -1,12 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Proxy Claude CLI
|
|
5
|
+
*
|
|
6
|
+
* Cross-platform CLI tool for running Claude Code with Antigravity proxy.
|
|
7
|
+
* Works on Windows, macOS, and Linux.
|
|
8
|
+
*/
|
|
9
|
+
|
|
3
10
|
import { fileURLToPath } from 'url';
|
|
4
11
|
import { dirname, join } from 'path';
|
|
5
|
-
import { readFileSync } from 'fs';
|
|
12
|
+
import { readFileSync, existsSync } from 'fs';
|
|
13
|
+
import { spawn, execSync } from 'child_process';
|
|
14
|
+
import os from 'os';
|
|
15
|
+
import http from 'http';
|
|
6
16
|
|
|
7
17
|
const __filename = fileURLToPath(import.meta.url);
|
|
8
18
|
const __dirname = dirname(__filename);
|
|
9
19
|
|
|
20
|
+
// Platform detection
|
|
21
|
+
const IS_WINDOWS = process.platform === 'win32';
|
|
22
|
+
const HOME_DIR = os.homedir();
|
|
23
|
+
|
|
24
|
+
// Config paths
|
|
25
|
+
const CONFIG_DIR = join(HOME_DIR, '.config', 'antigravity-proxy');
|
|
26
|
+
const ACCOUNTS_FILE = join(CONFIG_DIR, 'accounts.json');
|
|
27
|
+
const CLAUDE_SETTINGS = join(HOME_DIR, '.claude', 'settings.json');
|
|
28
|
+
|
|
10
29
|
// Read package.json for version
|
|
11
30
|
const packageJson = JSON.parse(
|
|
12
31
|
readFileSync(join(__dirname, '..', 'package.json'), 'utf-8')
|
|
@@ -15,46 +34,164 @@ const packageJson = JSON.parse(
|
|
|
15
34
|
const args = process.argv.slice(2);
|
|
16
35
|
const command = args[0];
|
|
17
36
|
|
|
37
|
+
// Colors (cross-platform)
|
|
38
|
+
const C = {
|
|
39
|
+
reset: '\x1b[0m',
|
|
40
|
+
bold: '\x1b[1m',
|
|
41
|
+
dim: '\x1b[2m',
|
|
42
|
+
red: '\x1b[31m',
|
|
43
|
+
green: '\x1b[32m',
|
|
44
|
+
yellow: '\x1b[33m',
|
|
45
|
+
blue: '\x1b[34m',
|
|
46
|
+
cyan: '\x1b[36m',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check if first run (no accounts or settings configured)
|
|
51
|
+
*/
|
|
52
|
+
function isFirstRun() {
|
|
53
|
+
try {
|
|
54
|
+
if (!existsSync(ACCOUNTS_FILE)) return true;
|
|
55
|
+
const accounts = JSON.parse(readFileSync(ACCOUNTS_FILE, 'utf-8'));
|
|
56
|
+
if (!accounts.accounts || accounts.accounts.length === 0) return true;
|
|
57
|
+
|
|
58
|
+
if (!existsSync(CLAUDE_SETTINGS)) return true;
|
|
59
|
+
const settings = JSON.parse(readFileSync(CLAUDE_SETTINGS, 'utf-8'));
|
|
60
|
+
if (!settings.env?.ANTHROPIC_BASE_URL) return true;
|
|
61
|
+
|
|
62
|
+
return false;
|
|
63
|
+
} catch {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if command exists (cross-platform)
|
|
70
|
+
*/
|
|
71
|
+
function commandExists(cmd) {
|
|
72
|
+
try {
|
|
73
|
+
const checkCmd = IS_WINDOWS ? `where ${cmd}` : `which ${cmd}`;
|
|
74
|
+
execSync(checkCmd, { stdio: 'ignore' });
|
|
75
|
+
return true;
|
|
76
|
+
} catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check if port is in use using Node.js (cross-platform)
|
|
83
|
+
*/
|
|
84
|
+
function checkPort(port) {
|
|
85
|
+
return new Promise((resolve) => {
|
|
86
|
+
const req = http.get(`http://localhost:${port}/health`, (res) => {
|
|
87
|
+
resolve(true);
|
|
88
|
+
res.resume();
|
|
89
|
+
});
|
|
90
|
+
req.on('error', () => resolve(false));
|
|
91
|
+
req.setTimeout(1000, () => {
|
|
92
|
+
req.destroy();
|
|
93
|
+
resolve(false);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Wait for proxy to be ready
|
|
100
|
+
*/
|
|
101
|
+
async function waitForProxy(port, maxAttempts = 60) {
|
|
102
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
103
|
+
if (await checkPort(port)) return true;
|
|
104
|
+
await new Promise(r => setTimeout(r, 500));
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Stop proxy server (cross-platform)
|
|
111
|
+
*/
|
|
112
|
+
async function stopProxy(port) {
|
|
113
|
+
if (IS_WINDOWS) {
|
|
114
|
+
try {
|
|
115
|
+
// Find PID using netstat on Windows
|
|
116
|
+
const output = execSync(`netstat -ano | findstr :${port} | findstr LISTENING`, {
|
|
117
|
+
encoding: 'utf-8',
|
|
118
|
+
stdio: ['pipe', 'pipe', 'ignore']
|
|
119
|
+
});
|
|
120
|
+
const lines = output.trim().split('\n');
|
|
121
|
+
for (const line of lines) {
|
|
122
|
+
const parts = line.trim().split(/\s+/);
|
|
123
|
+
const pid = parts[parts.length - 1];
|
|
124
|
+
if (pid && pid !== '0') {
|
|
125
|
+
try {
|
|
126
|
+
execSync(`taskkill /F /PID ${pid}`, { stdio: 'ignore' });
|
|
127
|
+
} catch {}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return true;
|
|
131
|
+
} catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
try {
|
|
136
|
+
execSync(`lsof -ti tcp:${port} | xargs kill 2>/dev/null`, {
|
|
137
|
+
stdio: 'ignore',
|
|
138
|
+
shell: true
|
|
139
|
+
});
|
|
140
|
+
return true;
|
|
141
|
+
} catch {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get port from args or env
|
|
149
|
+
*/
|
|
150
|
+
function getPort() {
|
|
151
|
+
const portIndex = args.findIndex(a => a === '--port' || a === '-p');
|
|
152
|
+
if (portIndex !== -1 && args[portIndex + 1]) {
|
|
153
|
+
return parseInt(args[portIndex + 1], 10);
|
|
154
|
+
}
|
|
155
|
+
return parseInt(process.env.PORT || '8080', 10);
|
|
156
|
+
}
|
|
157
|
+
|
|
18
158
|
function showHelp() {
|
|
19
159
|
console.log(`
|
|
20
|
-
proxy-claude v${packageJson.version}
|
|
160
|
+
${C.cyan}proxy-claude${C.reset} v${packageJson.version}
|
|
21
161
|
|
|
22
162
|
Proxy server for using Antigravity's Claude models with Claude Code CLI.
|
|
23
163
|
|
|
24
|
-
USAGE
|
|
25
|
-
proxy-claude
|
|
26
|
-
|
|
27
|
-
COMMANDS
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
accounts
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
OPTIONS:
|
|
164
|
+
${C.bold}USAGE:${C.reset}
|
|
165
|
+
proxy-claude [command] [options]
|
|
166
|
+
|
|
167
|
+
${C.bold}COMMANDS:${C.reset}
|
|
168
|
+
${C.cyan}(default)${C.reset} Start proxy + launch Claude Code
|
|
169
|
+
${C.cyan}init${C.reset} Run setup wizard (configure models & accounts)
|
|
170
|
+
${C.cyan}start${C.reset} Start the proxy server only
|
|
171
|
+
${C.cyan}stop${C.reset} Stop running proxy server
|
|
172
|
+
${C.cyan}status${C.reset} Check if proxy is running
|
|
173
|
+
${C.cyan}accounts${C.reset} Manage Google accounts (add/list/remove/verify)
|
|
174
|
+
${C.cyan}refresh${C.reset} Check and refresh account tokens
|
|
175
|
+
|
|
176
|
+
${C.bold}OPTIONS:${C.reset}
|
|
39
177
|
--help, -h Show this help message
|
|
40
178
|
--version, -v Show version number
|
|
179
|
+
--port, -p <port> Set custom port (default: 8080)
|
|
180
|
+
--force Force reconfigure (with init)
|
|
41
181
|
|
|
42
|
-
ENVIRONMENT
|
|
182
|
+
${C.bold}ENVIRONMENT:${C.reset}
|
|
43
183
|
PORT Server port (default: 8080)
|
|
44
184
|
|
|
45
|
-
EXAMPLES
|
|
46
|
-
proxy-claude
|
|
47
|
-
|
|
48
|
-
proxy-claude
|
|
49
|
-
proxy-claude
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
"ANTHROPIC_BASE_URL": "http://localhost:8080"
|
|
56
|
-
}
|
|
57
|
-
}
|
|
185
|
+
${C.bold}EXAMPLES:${C.reset}
|
|
186
|
+
proxy-claude # Start proxy + Claude Code
|
|
187
|
+
proxy-claude init # Run setup wizard
|
|
188
|
+
proxy-claude start # Start proxy server only
|
|
189
|
+
proxy-claude stop # Stop proxy server
|
|
190
|
+
PORT=3000 proxy-claude # Use custom port
|
|
191
|
+
proxy-claude accounts add # Add Google account
|
|
192
|
+
|
|
193
|
+
${C.bold}FIRST TIME?${C.reset}
|
|
194
|
+
Run ${C.cyan}proxy-claude init${C.reset} to configure everything.
|
|
58
195
|
`);
|
|
59
196
|
}
|
|
60
197
|
|
|
@@ -62,6 +199,9 @@ function showVersion() {
|
|
|
62
199
|
console.log(packageJson.version);
|
|
63
200
|
}
|
|
64
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Main CLI handler
|
|
204
|
+
*/
|
|
65
205
|
async function main() {
|
|
66
206
|
// Handle flags
|
|
67
207
|
if (args.includes('--help') || args.includes('-h')) {
|
|
@@ -74,17 +214,54 @@ async function main() {
|
|
|
74
214
|
process.exit(0);
|
|
75
215
|
}
|
|
76
216
|
|
|
217
|
+
const port = getPort();
|
|
218
|
+
|
|
77
219
|
// Handle commands
|
|
78
220
|
switch (command) {
|
|
79
|
-
case '
|
|
80
|
-
|
|
221
|
+
case 'init':
|
|
222
|
+
case 'setup': {
|
|
223
|
+
// Run onboarding wizard
|
|
224
|
+
const force = args.includes('--force') || args.includes('-f');
|
|
225
|
+
const { runOnboarding } = await import('../src/cli/onboard.js');
|
|
226
|
+
const success = await runOnboarding({ skipIfConfigured: !force });
|
|
227
|
+
process.exit(success ? 0 : 1);
|
|
81
228
|
break;
|
|
229
|
+
}
|
|
82
230
|
|
|
83
231
|
case 'start':
|
|
84
|
-
case
|
|
85
|
-
//
|
|
232
|
+
case 'web': {
|
|
233
|
+
// Start the server only
|
|
86
234
|
await import('../src/index.js');
|
|
87
235
|
break;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
case 'stop': {
|
|
239
|
+
// Stop running proxy
|
|
240
|
+
console.log(`${C.yellow}Stopping proxy on port ${port}...${C.reset}`);
|
|
241
|
+
const stopped = await stopProxy(port);
|
|
242
|
+
if (stopped) {
|
|
243
|
+
console.log(`${C.green}✓ Proxy stopped${C.reset}`);
|
|
244
|
+
} else {
|
|
245
|
+
console.log(`${C.dim}No proxy found running on port ${port}${C.reset}`);
|
|
246
|
+
}
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
case 'status': {
|
|
251
|
+
// Check if proxy is running
|
|
252
|
+
const running = await checkPort(port);
|
|
253
|
+
if (running) {
|
|
254
|
+
console.log(`${C.green}✓ Proxy is running on port ${port}${C.reset}`);
|
|
255
|
+
try {
|
|
256
|
+
const res = await fetch(`http://localhost:${port}/health`);
|
|
257
|
+
const data = await res.json();
|
|
258
|
+
console.log(` ${C.dim}Status: ${data.summary}${C.reset}`);
|
|
259
|
+
} catch {}
|
|
260
|
+
} else {
|
|
261
|
+
console.log(`${C.yellow}✗ Proxy is not running on port ${port}${C.reset}`);
|
|
262
|
+
}
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
88
265
|
|
|
89
266
|
case 'accounts': {
|
|
90
267
|
// Pass remaining args to accounts CLI
|
|
@@ -111,14 +288,116 @@ async function main() {
|
|
|
111
288
|
showVersion();
|
|
112
289
|
break;
|
|
113
290
|
|
|
291
|
+
case undefined:
|
|
292
|
+
case 'run': {
|
|
293
|
+
// Check if first run - prompt for setup
|
|
294
|
+
if (isFirstRun()) {
|
|
295
|
+
console.log(`${C.cyan}Welcome to Proxy Claude!${C.reset}`);
|
|
296
|
+
console.log(`${C.dim}It looks like this is your first time running.${C.reset}`);
|
|
297
|
+
console.log('');
|
|
298
|
+
console.log(`Running setup wizard...`);
|
|
299
|
+
console.log('');
|
|
300
|
+
|
|
301
|
+
const { runOnboarding } = await import('../src/cli/onboard.js');
|
|
302
|
+
const success = await runOnboarding();
|
|
303
|
+
if (!success) {
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
console.log('');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Default: Start proxy in background + launch Claude Code
|
|
310
|
+
console.log(`${C.blue}Starting Antigravity Claude Proxy on port ${port}...${C.reset}`);
|
|
311
|
+
|
|
312
|
+
// Check if already running
|
|
313
|
+
if (await checkPort(port)) {
|
|
314
|
+
console.log(`${C.green}✓ Proxy already running on port ${port}${C.reset}`);
|
|
315
|
+
} else {
|
|
316
|
+
// Start proxy in background
|
|
317
|
+
const proxyScript = join(__dirname, '..', 'src', 'index.js');
|
|
318
|
+
const proxyProcess = spawn(process.execPath, [proxyScript], {
|
|
319
|
+
detached: true,
|
|
320
|
+
stdio: 'ignore',
|
|
321
|
+
env: { ...process.env, PORT: String(port) },
|
|
322
|
+
shell: false,
|
|
323
|
+
windowsHide: true,
|
|
324
|
+
});
|
|
325
|
+
proxyProcess.unref();
|
|
326
|
+
|
|
327
|
+
// Wait for proxy to be ready
|
|
328
|
+
console.log('Waiting for proxy to be ready...');
|
|
329
|
+
const ready = await waitForProxy(port);
|
|
330
|
+
|
|
331
|
+
if (!ready) {
|
|
332
|
+
console.error(`${C.red}Error: Proxy failed to start within 30 seconds.${C.reset}`);
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
console.log(`${C.green}✓ Proxy is ready on port ${port}!${C.reset}`);
|
|
337
|
+
}
|
|
338
|
+
console.log('');
|
|
339
|
+
|
|
340
|
+
// Check if claude is installed
|
|
341
|
+
if (!commandExists('claude')) {
|
|
342
|
+
console.error(`${C.red}Error: Claude Code CLI not found.${C.reset}`);
|
|
343
|
+
console.error('Install it with: npm install -g @anthropic-ai/claude-code');
|
|
344
|
+
console.error('Or run: proxy-claude init');
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Launch Claude with proxy config
|
|
349
|
+
const claudeArgs = args.slice(command === 'run' ? 1 : 0).filter(a =>
|
|
350
|
+
a !== '--port' && a !== '-p' && !args[args.indexOf('--port') + 1]?.includes(a)
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
const claudeProcess = spawn('claude', claudeArgs, {
|
|
354
|
+
stdio: 'inherit',
|
|
355
|
+
env: {
|
|
356
|
+
...process.env,
|
|
357
|
+
ANTHROPIC_BASE_URL: `http://localhost:${port}`,
|
|
358
|
+
ANTHROPIC_API_KEY: 'proxy-claude',
|
|
359
|
+
},
|
|
360
|
+
shell: IS_WINDOWS,
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// Cleanup on exit
|
|
364
|
+
const cleanup = async () => {
|
|
365
|
+
console.log(`\n${C.yellow}Stopping Antigravity Claude Proxy...${C.reset}`);
|
|
366
|
+
await stopProxy(port);
|
|
367
|
+
console.log(`${C.green}✓ Proxy stopped${C.reset}`);
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
claudeProcess.on('close', async (code) => {
|
|
371
|
+
await cleanup();
|
|
372
|
+
process.exit(code || 0);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// Handle signals
|
|
376
|
+
const handleSignal = (signal) => {
|
|
377
|
+
claudeProcess.kill(signal);
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
process.on('SIGINT', () => handleSignal('SIGINT'));
|
|
381
|
+
process.on('SIGTERM', () => handleSignal('SIGTERM'));
|
|
382
|
+
|
|
383
|
+
// Windows-specific handling
|
|
384
|
+
if (IS_WINDOWS) {
|
|
385
|
+
process.on('SIGHUP', () => handleSignal('SIGHUP'));
|
|
386
|
+
}
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
|
|
114
390
|
default:
|
|
115
|
-
console.error(
|
|
391
|
+
console.error(`${C.red}Unknown command: ${command}${C.reset}`);
|
|
116
392
|
console.error('Run "proxy-claude --help" for usage information.');
|
|
117
393
|
process.exit(1);
|
|
118
394
|
}
|
|
119
395
|
}
|
|
120
396
|
|
|
121
397
|
main().catch((err) => {
|
|
122
|
-
console.error(
|
|
398
|
+
console.error(`${C.red}Error:${C.reset}`, err.message);
|
|
399
|
+
if (process.env.DEBUG) {
|
|
400
|
+
console.error(err.stack);
|
|
401
|
+
}
|
|
123
402
|
process.exit(1);
|
|
124
403
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kamel-ahmed/proxy-claude",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Proxy server to use Antigravity's Claude models with Claude Code CLI - run 'proxy-claude' to start",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"accounts:verify": "node src/cli/accounts.js verify",
|
|
27
27
|
"refresh": "node src/cli/refresh.js",
|
|
28
28
|
"refresh:force": "node src/cli/refresh.js --force",
|
|
29
|
-
"setup": "node bin/cli.js
|
|
29
|
+
"setup": "node bin/cli.js init",
|
|
30
30
|
"test": "node tests/run-all.cjs",
|
|
31
31
|
"test:signatures": "node tests/test-thinking-signatures.cjs",
|
|
32
32
|
"test:multiturn": "node tests/test-multiturn-thinking-tools.cjs",
|
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Professional CLI Onboarding
|
|
3
|
+
*
|
|
4
|
+
* Cross-platform setup wizard that configures everything needed
|
|
5
|
+
* to run Claude Code with the Antigravity proxy.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execSync, spawn } from 'child_process';
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import os from 'os';
|
|
12
|
+
import readline from 'readline';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = path.dirname(__filename);
|
|
17
|
+
|
|
18
|
+
// Colors (cross-platform safe)
|
|
19
|
+
const supportsColor = process.stdout.isTTY && (
|
|
20
|
+
process.env.COLORTERM === 'truecolor' ||
|
|
21
|
+
process.env.TERM?.includes('256color') ||
|
|
22
|
+
process.platform === 'win32'
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const COLORS = {
|
|
26
|
+
reset: supportsColor ? '\x1b[0m' : '',
|
|
27
|
+
bold: supportsColor ? '\x1b[1m' : '',
|
|
28
|
+
dim: supportsColor ? '\x1b[2m' : '',
|
|
29
|
+
red: supportsColor ? '\x1b[31m' : '',
|
|
30
|
+
green: supportsColor ? '\x1b[32m' : '',
|
|
31
|
+
yellow: supportsColor ? '\x1b[33m' : '',
|
|
32
|
+
blue: supportsColor ? '\x1b[34m' : '',
|
|
33
|
+
magenta: supportsColor ? '\x1b[35m' : '',
|
|
34
|
+
cyan: supportsColor ? '\x1b[36m' : '',
|
|
35
|
+
white: supportsColor ? '\x1b[37m' : '',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Platform detection
|
|
39
|
+
const PLATFORM = {
|
|
40
|
+
isWindows: process.platform === 'win32',
|
|
41
|
+
isMac: process.platform === 'darwin',
|
|
42
|
+
isLinux: process.platform === 'linux',
|
|
43
|
+
homeDir: os.homedir(),
|
|
44
|
+
pathSep: path.sep,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Paths
|
|
48
|
+
const CONFIG_DIR = path.join(PLATFORM.homeDir, '.config', 'antigravity-proxy');
|
|
49
|
+
const ACCOUNTS_FILE = path.join(CONFIG_DIR, 'accounts.json');
|
|
50
|
+
const CLAUDE_CONFIG_DIR = path.join(PLATFORM.homeDir, '.claude');
|
|
51
|
+
const CLAUDE_SETTINGS_FILE = path.join(CLAUDE_CONFIG_DIR, 'settings.json');
|
|
52
|
+
|
|
53
|
+
// Default model configurations
|
|
54
|
+
const MODEL_PRESETS = {
|
|
55
|
+
claude: {
|
|
56
|
+
name: 'Claude Models (Recommended)',
|
|
57
|
+
description: 'Best for coding tasks with extended thinking',
|
|
58
|
+
models: {
|
|
59
|
+
ANTHROPIC_MODEL: 'claude-sonnet-4-5-thinking',
|
|
60
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4-5-thinking',
|
|
61
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4-5-thinking',
|
|
62
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gemini-2.5-flash-lite',
|
|
63
|
+
CLAUDE_CODE_SUBAGENT_MODEL: 'claude-sonnet-4-5-thinking',
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
gemini: {
|
|
67
|
+
name: 'Gemini Models',
|
|
68
|
+
description: 'Google\'s Gemini models with thinking support',
|
|
69
|
+
models: {
|
|
70
|
+
ANTHROPIC_MODEL: 'gemini-3-pro-high',
|
|
71
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: 'gemini-3-pro-high',
|
|
72
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: 'gemini-3-flash',
|
|
73
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gemini-2.5-flash-lite',
|
|
74
|
+
CLAUDE_CODE_SUBAGENT_MODEL: 'gemini-3-flash',
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
balanced: {
|
|
78
|
+
name: 'Balanced (Claude + Gemini)',
|
|
79
|
+
description: 'Claude for main tasks, Gemini for background',
|
|
80
|
+
models: {
|
|
81
|
+
ANTHROPIC_MODEL: 'claude-sonnet-4-5-thinking',
|
|
82
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: 'claude-opus-4-5-thinking',
|
|
83
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4-5-thinking',
|
|
84
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gemini-2.5-flash-lite',
|
|
85
|
+
CLAUDE_CODE_SUBAGENT_MODEL: 'gemini-3-flash',
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Create readline interface for user input
|
|
92
|
+
*/
|
|
93
|
+
function createPrompt() {
|
|
94
|
+
return readline.createInterface({
|
|
95
|
+
input: process.stdin,
|
|
96
|
+
output: process.stdout,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Prompt user for input
|
|
102
|
+
*/
|
|
103
|
+
async function prompt(rl, question, defaultValue = '') {
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
const defaultText = defaultValue ? ` ${COLORS.dim}(${defaultValue})${COLORS.reset}` : '';
|
|
106
|
+
rl.question(`${question}${defaultText}: `, (answer) => {
|
|
107
|
+
resolve(answer.trim() || defaultValue);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Prompt for yes/no
|
|
114
|
+
*/
|
|
115
|
+
async function confirm(rl, question, defaultYes = true) {
|
|
116
|
+
const hint = defaultYes ? 'Y/n' : 'y/N';
|
|
117
|
+
const answer = await prompt(rl, `${question} [${hint}]`);
|
|
118
|
+
if (!answer) return defaultYes;
|
|
119
|
+
return answer.toLowerCase().startsWith('y');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Print styled message
|
|
124
|
+
*/
|
|
125
|
+
function print(message, type = 'info') {
|
|
126
|
+
const icons = {
|
|
127
|
+
info: `${COLORS.blue}ℹ${COLORS.reset}`,
|
|
128
|
+
success: `${COLORS.green}✓${COLORS.reset}`,
|
|
129
|
+
warn: `${COLORS.yellow}⚠${COLORS.reset}`,
|
|
130
|
+
error: `${COLORS.red}✗${COLORS.reset}`,
|
|
131
|
+
step: `${COLORS.cyan}▸${COLORS.reset}`,
|
|
132
|
+
};
|
|
133
|
+
console.log(`${icons[type] || ''} ${message}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Print header
|
|
138
|
+
*/
|
|
139
|
+
function printHeader() {
|
|
140
|
+
console.log('');
|
|
141
|
+
console.log(`${COLORS.cyan}╔══════════════════════════════════════════════════════════╗${COLORS.reset}`);
|
|
142
|
+
console.log(`${COLORS.cyan}║${COLORS.reset} ${COLORS.bold}Proxy Claude - Setup Wizard${COLORS.reset} ${COLORS.cyan}║${COLORS.reset}`);
|
|
143
|
+
console.log(`${COLORS.cyan}║${COLORS.reset} ${COLORS.dim}Configure everything in one go${COLORS.reset} ${COLORS.cyan}║${COLORS.reset}`);
|
|
144
|
+
console.log(`${COLORS.cyan}╚══════════════════════════════════════════════════════════╝${COLORS.reset}`);
|
|
145
|
+
console.log('');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Print step header
|
|
150
|
+
*/
|
|
151
|
+
function printStep(step, total, title) {
|
|
152
|
+
console.log('');
|
|
153
|
+
console.log(`${COLORS.bold}${COLORS.blue}[${step}/${total}]${COLORS.reset} ${COLORS.bold}${title}${COLORS.reset}`);
|
|
154
|
+
console.log(`${COLORS.dim}${'─'.repeat(50)}${COLORS.reset}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Check if a command exists
|
|
159
|
+
*/
|
|
160
|
+
function commandExists(cmd) {
|
|
161
|
+
try {
|
|
162
|
+
const checkCmd = PLATFORM.isWindows ? `where ${cmd}` : `which ${cmd}`;
|
|
163
|
+
execSync(checkCmd, { stdio: 'ignore' });
|
|
164
|
+
return true;
|
|
165
|
+
} catch {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Check Node.js version
|
|
172
|
+
*/
|
|
173
|
+
function checkNodeVersion() {
|
|
174
|
+
const version = process.version;
|
|
175
|
+
const major = parseInt(version.slice(1).split('.')[0], 10);
|
|
176
|
+
return { version, major, ok: major >= 18 };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Check if Claude Code CLI is installed
|
|
181
|
+
*/
|
|
182
|
+
function checkClaudeCli() {
|
|
183
|
+
return commandExists('claude');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Install Claude Code CLI
|
|
188
|
+
*/
|
|
189
|
+
async function installClaudeCli() {
|
|
190
|
+
print('Installing Claude Code CLI...', 'step');
|
|
191
|
+
try {
|
|
192
|
+
execSync('npm install -g @anthropic-ai/claude-code', {
|
|
193
|
+
stdio: 'inherit',
|
|
194
|
+
shell: true
|
|
195
|
+
});
|
|
196
|
+
print('Claude Code CLI installed successfully', 'success');
|
|
197
|
+
return true;
|
|
198
|
+
} catch (error) {
|
|
199
|
+
print(`Failed to install Claude Code CLI: ${error.message}`, 'error');
|
|
200
|
+
print('Please install manually: npm install -g @anthropic-ai/claude-code', 'info');
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Check for existing accounts
|
|
207
|
+
*/
|
|
208
|
+
function getExistingAccounts() {
|
|
209
|
+
try {
|
|
210
|
+
if (fs.existsSync(ACCOUNTS_FILE)) {
|
|
211
|
+
const data = JSON.parse(fs.readFileSync(ACCOUNTS_FILE, 'utf-8'));
|
|
212
|
+
return data.accounts || [];
|
|
213
|
+
}
|
|
214
|
+
} catch {}
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Add Google account via OAuth
|
|
220
|
+
*/
|
|
221
|
+
async function addGoogleAccount() {
|
|
222
|
+
print('Starting Google OAuth flow...', 'step');
|
|
223
|
+
print('A browser window will open for authentication.', 'info');
|
|
224
|
+
console.log('');
|
|
225
|
+
|
|
226
|
+
return new Promise((resolve) => {
|
|
227
|
+
const accountsScript = path.join(__dirname, 'accounts.js');
|
|
228
|
+
const child = spawn('node', [accountsScript, 'add'], {
|
|
229
|
+
stdio: 'inherit',
|
|
230
|
+
shell: true,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
child.on('close', (code) => {
|
|
234
|
+
resolve(code === 0);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Load existing Claude settings
|
|
241
|
+
*/
|
|
242
|
+
function loadClaudeSettings() {
|
|
243
|
+
try {
|
|
244
|
+
if (fs.existsSync(CLAUDE_SETTINGS_FILE)) {
|
|
245
|
+
return JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_FILE, 'utf-8'));
|
|
246
|
+
}
|
|
247
|
+
} catch {}
|
|
248
|
+
return {};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Save Claude settings
|
|
253
|
+
*/
|
|
254
|
+
function saveClaudeSettings(settings) {
|
|
255
|
+
// Ensure directory exists
|
|
256
|
+
if (!fs.existsSync(CLAUDE_CONFIG_DIR)) {
|
|
257
|
+
fs.mkdirSync(CLAUDE_CONFIG_DIR, { recursive: true });
|
|
258
|
+
}
|
|
259
|
+
fs.writeFileSync(CLAUDE_SETTINGS_FILE, JSON.stringify(settings, null, 2));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Configure Claude Code settings
|
|
264
|
+
*/
|
|
265
|
+
function configureClaudeSettings(modelPreset, port = 8080) {
|
|
266
|
+
const existing = loadClaudeSettings();
|
|
267
|
+
|
|
268
|
+
const newSettings = {
|
|
269
|
+
...existing,
|
|
270
|
+
env: {
|
|
271
|
+
...(existing.env || {}),
|
|
272
|
+
ANTHROPIC_AUTH_TOKEN: 'proxy-claude',
|
|
273
|
+
ANTHROPIC_BASE_URL: `http://localhost:${port}`,
|
|
274
|
+
...modelPreset.models,
|
|
275
|
+
ENABLE_EXPERIMENTAL_MCP_CLI: 'true',
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// Also set hasCompletedOnboarding to skip Claude's own onboarding
|
|
280
|
+
if (!existing.hasCompletedOnboarding) {
|
|
281
|
+
newSettings.hasCompletedOnboarding = true;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
saveClaudeSettings(newSettings);
|
|
285
|
+
return newSettings;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Display model selection menu
|
|
290
|
+
*/
|
|
291
|
+
async function selectModelPreset(rl) {
|
|
292
|
+
console.log('');
|
|
293
|
+
console.log(`${COLORS.bold}Available model configurations:${COLORS.reset}`);
|
|
294
|
+
console.log('');
|
|
295
|
+
|
|
296
|
+
const presets = Object.entries(MODEL_PRESETS);
|
|
297
|
+
presets.forEach(([key, preset], index) => {
|
|
298
|
+
console.log(` ${COLORS.cyan}${index + 1}.${COLORS.reset} ${COLORS.bold}${preset.name}${COLORS.reset}`);
|
|
299
|
+
console.log(` ${COLORS.dim}${preset.description}${COLORS.reset}`);
|
|
300
|
+
});
|
|
301
|
+
console.log('');
|
|
302
|
+
|
|
303
|
+
const choice = await prompt(rl, 'Select configuration (1-3)', '1');
|
|
304
|
+
const index = parseInt(choice, 10) - 1;
|
|
305
|
+
|
|
306
|
+
if (index >= 0 && index < presets.length) {
|
|
307
|
+
return presets[index];
|
|
308
|
+
}
|
|
309
|
+
return presets[0]; // Default to first option
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Print summary
|
|
314
|
+
*/
|
|
315
|
+
function printSummary(accounts, modelPreset, port) {
|
|
316
|
+
console.log('');
|
|
317
|
+
console.log(`${COLORS.green}╔══════════════════════════════════════════════════════════╗${COLORS.reset}`);
|
|
318
|
+
console.log(`${COLORS.green}║${COLORS.reset} ${COLORS.bold}${COLORS.green}Setup Complete!${COLORS.reset} ${COLORS.green}║${COLORS.reset}`);
|
|
319
|
+
console.log(`${COLORS.green}╚══════════════════════════════════════════════════════════╝${COLORS.reset}`);
|
|
320
|
+
console.log('');
|
|
321
|
+
|
|
322
|
+
console.log(`${COLORS.bold}Configuration Summary:${COLORS.reset}`);
|
|
323
|
+
console.log(` ${COLORS.dim}•${COLORS.reset} Accounts: ${accounts.length} configured`);
|
|
324
|
+
console.log(` ${COLORS.dim}•${COLORS.reset} Model preset: ${modelPreset[1].name}`);
|
|
325
|
+
console.log(` ${COLORS.dim}•${COLORS.reset} Proxy port: ${port}`);
|
|
326
|
+
console.log(` ${COLORS.dim}•${COLORS.reset} Settings saved to: ${CLAUDE_SETTINGS_FILE}`);
|
|
327
|
+
console.log('');
|
|
328
|
+
|
|
329
|
+
console.log(`${COLORS.bold}To start using Claude with the proxy:${COLORS.reset}`);
|
|
330
|
+
console.log('');
|
|
331
|
+
console.log(` ${COLORS.cyan}proxy-claude${COLORS.reset}`);
|
|
332
|
+
console.log('');
|
|
333
|
+
console.log(`${COLORS.dim}Or start components separately:${COLORS.reset}`);
|
|
334
|
+
console.log(` ${COLORS.dim}proxy-claude start${COLORS.reset} - Start proxy server only`);
|
|
335
|
+
console.log(` ${COLORS.dim}proxy-claude web${COLORS.reset} - Start with web dashboard`);
|
|
336
|
+
console.log('');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Main onboarding flow
|
|
341
|
+
*/
|
|
342
|
+
export async function runOnboarding(options = {}) {
|
|
343
|
+
const { skipIfConfigured = false, quiet = false } = options;
|
|
344
|
+
|
|
345
|
+
// Check if already configured
|
|
346
|
+
const existingAccounts = getExistingAccounts();
|
|
347
|
+
const existingSettings = loadClaudeSettings();
|
|
348
|
+
const isConfigured = existingAccounts.length > 0 && existingSettings.env?.ANTHROPIC_BASE_URL;
|
|
349
|
+
|
|
350
|
+
if (skipIfConfigured && isConfigured) {
|
|
351
|
+
if (!quiet) {
|
|
352
|
+
print('Already configured. Use --force to reconfigure.', 'info');
|
|
353
|
+
}
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
printHeader();
|
|
358
|
+
|
|
359
|
+
const rl = createPrompt();
|
|
360
|
+
const totalSteps = 4;
|
|
361
|
+
let currentStep = 0;
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
// Step 1: Check prerequisites
|
|
365
|
+
printStep(++currentStep, totalSteps, 'Checking Prerequisites');
|
|
366
|
+
|
|
367
|
+
// Check Node.js
|
|
368
|
+
const nodeCheck = checkNodeVersion();
|
|
369
|
+
if (nodeCheck.ok) {
|
|
370
|
+
print(`Node.js ${nodeCheck.version} ✓`, 'success');
|
|
371
|
+
} else {
|
|
372
|
+
print(`Node.js ${nodeCheck.version} - version 18+ required`, 'error');
|
|
373
|
+
rl.close();
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Check/Install Claude CLI
|
|
378
|
+
if (checkClaudeCli()) {
|
|
379
|
+
print('Claude Code CLI ✓', 'success');
|
|
380
|
+
} else {
|
|
381
|
+
print('Claude Code CLI not found', 'warn');
|
|
382
|
+
const install = await confirm(rl, 'Install Claude Code CLI now?');
|
|
383
|
+
if (install) {
|
|
384
|
+
const installed = await installClaudeCli();
|
|
385
|
+
if (!installed) {
|
|
386
|
+
rl.close();
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
} else {
|
|
390
|
+
print('Claude Code CLI is required. Please install it first.', 'error');
|
|
391
|
+
rl.close();
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Step 2: Account Setup
|
|
397
|
+
printStep(++currentStep, totalSteps, 'Account Configuration');
|
|
398
|
+
|
|
399
|
+
if (existingAccounts.length > 0) {
|
|
400
|
+
print(`Found ${existingAccounts.length} existing account(s):`, 'info');
|
|
401
|
+
existingAccounts.forEach((acc, i) => {
|
|
402
|
+
console.log(` ${COLORS.dim}${i + 1}.${COLORS.reset} ${acc.email}`);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const addMore = await confirm(rl, 'Add another account?', false);
|
|
406
|
+
if (addMore) {
|
|
407
|
+
await addGoogleAccount();
|
|
408
|
+
}
|
|
409
|
+
} else {
|
|
410
|
+
print('No accounts configured yet.', 'info');
|
|
411
|
+
const addAccount = await confirm(rl, 'Add a Google account now?');
|
|
412
|
+
if (addAccount) {
|
|
413
|
+
await addGoogleAccount();
|
|
414
|
+
} else {
|
|
415
|
+
print('You can add accounts later with: proxy-claude accounts add', 'info');
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Reload accounts after potential additions
|
|
420
|
+
const accounts = getExistingAccounts();
|
|
421
|
+
|
|
422
|
+
// Step 3: Model Configuration
|
|
423
|
+
printStep(++currentStep, totalSteps, 'Model Configuration');
|
|
424
|
+
|
|
425
|
+
print('Select which AI models to use with Claude Code.', 'info');
|
|
426
|
+
print(`${COLORS.dim}(Haiku model uses Gemini to preserve Claude quota)${COLORS.reset}`, 'info');
|
|
427
|
+
|
|
428
|
+
const modelPreset = await selectModelPreset(rl);
|
|
429
|
+
print(`Selected: ${modelPreset[1].name}`, 'success');
|
|
430
|
+
|
|
431
|
+
// Step 4: Apply Configuration
|
|
432
|
+
printStep(++currentStep, totalSteps, 'Applying Configuration');
|
|
433
|
+
|
|
434
|
+
const port = parseInt(process.env.PORT || '8080', 10);
|
|
435
|
+
|
|
436
|
+
// Configure Claude Code settings
|
|
437
|
+
print('Configuring Claude Code settings...', 'step');
|
|
438
|
+
configureClaudeSettings(modelPreset[1], port);
|
|
439
|
+
print(`Settings saved to ${CLAUDE_SETTINGS_FILE}`, 'success');
|
|
440
|
+
|
|
441
|
+
// Print summary
|
|
442
|
+
printSummary(accounts, modelPreset, port);
|
|
443
|
+
|
|
444
|
+
rl.close();
|
|
445
|
+
return true;
|
|
446
|
+
|
|
447
|
+
} catch (error) {
|
|
448
|
+
print(`Setup error: ${error.message}`, 'error');
|
|
449
|
+
rl.close();
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Check if first run (no config exists)
|
|
456
|
+
*/
|
|
457
|
+
export function isFirstRun() {
|
|
458
|
+
const accounts = getExistingAccounts();
|
|
459
|
+
const settings = loadClaudeSettings();
|
|
460
|
+
return accounts.length === 0 || !settings.env?.ANTHROPIC_BASE_URL;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// CLI entry point
|
|
464
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
465
|
+
const force = process.argv.includes('--force') || process.argv.includes('-f');
|
|
466
|
+
runOnboarding({ skipIfConfigured: !force }).then((success) => {
|
|
467
|
+
process.exit(success ? 0 : 1);
|
|
468
|
+
});
|
|
469
|
+
}
|