@kamel-ahmed/proxy-claude 1.0.3 → 1.0.5
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 +301 -109
- package/package.json +2 -2
- package/src/cli/onboard.js +580 -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,50 +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
|
|
164
|
+
${C.bold}USAGE:${C.reset}
|
|
25
165
|
proxy-claude [command] [options]
|
|
26
166
|
|
|
27
|
-
COMMANDS
|
|
28
|
-
(default) Start proxy + launch Claude Code
|
|
29
|
-
|
|
30
|
-
start Start the proxy server only
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
accounts
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
accounts clear Remove all accounts
|
|
38
|
-
refresh Check and refresh account tokens
|
|
39
|
-
setup Install Claude Code CLI (if needed)
|
|
40
|
-
|
|
41
|
-
OPTIONS:
|
|
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}
|
|
42
177
|
--help, -h Show this help message
|
|
43
178
|
--version, -v Show version number
|
|
179
|
+
--port, -p <port> Set custom port (default: 8080)
|
|
180
|
+
--force Force reconfigure (with init)
|
|
44
181
|
|
|
45
|
-
ENVIRONMENT
|
|
182
|
+
${C.bold}ENVIRONMENT:${C.reset}
|
|
46
183
|
PORT Server port (default: 8080)
|
|
47
184
|
|
|
48
|
-
EXAMPLES
|
|
185
|
+
${C.bold}EXAMPLES:${C.reset}
|
|
49
186
|
proxy-claude # Start proxy + Claude Code
|
|
50
|
-
proxy-claude
|
|
187
|
+
proxy-claude init # Run setup wizard
|
|
51
188
|
proxy-claude start # Start proxy server only
|
|
189
|
+
proxy-claude stop # Stop proxy server
|
|
52
190
|
PORT=3000 proxy-claude # Use custom port
|
|
53
191
|
proxy-claude accounts add # Add Google account
|
|
54
192
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
{
|
|
58
|
-
"env": {
|
|
59
|
-
"ANTHROPIC_BASE_URL": "http://localhost:8080"
|
|
60
|
-
}
|
|
61
|
-
}
|
|
193
|
+
${C.bold}FIRST TIME?${C.reset}
|
|
194
|
+
Run ${C.cyan}proxy-claude init${C.reset} to configure everything.
|
|
62
195
|
`);
|
|
63
196
|
}
|
|
64
197
|
|
|
@@ -66,6 +199,9 @@ function showVersion() {
|
|
|
66
199
|
console.log(packageJson.version);
|
|
67
200
|
}
|
|
68
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Main CLI handler
|
|
204
|
+
*/
|
|
69
205
|
async function main() {
|
|
70
206
|
// Handle flags
|
|
71
207
|
if (args.includes('--help') || args.includes('-h')) {
|
|
@@ -78,98 +214,52 @@ async function main() {
|
|
|
78
214
|
process.exit(0);
|
|
79
215
|
}
|
|
80
216
|
|
|
217
|
+
const port = getPort();
|
|
218
|
+
|
|
81
219
|
// Handle commands
|
|
82
220
|
switch (command) {
|
|
83
|
-
case '
|
|
84
|
-
|
|
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);
|
|
85
228
|
break;
|
|
229
|
+
}
|
|
86
230
|
|
|
87
231
|
case 'start':
|
|
232
|
+
case 'web': {
|
|
88
233
|
// Start the server only
|
|
89
234
|
await import('../src/index.js');
|
|
90
235
|
break;
|
|
236
|
+
}
|
|
91
237
|
|
|
92
|
-
case '
|
|
93
|
-
//
|
|
94
|
-
|
|
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
|
+
}
|
|
95
247
|
break;
|
|
248
|
+
}
|
|
96
249
|
|
|
97
|
-
case
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
console.log(`\x1b[34mStarting Antigravity Claude Proxy on port ${port}...\x1b[0m`);
|
|
104
|
-
|
|
105
|
-
// Start proxy in background
|
|
106
|
-
const proxyProcess = spawn('node', [join(__dirname, '..', 'src', 'index.js')], {
|
|
107
|
-
detached: true,
|
|
108
|
-
stdio: 'ignore',
|
|
109
|
-
env: { ...process.env, PORT: port }
|
|
110
|
-
});
|
|
111
|
-
proxyProcess.unref();
|
|
112
|
-
|
|
113
|
-
// Wait for proxy to be ready
|
|
114
|
-
console.log('Waiting for proxy to be ready...');
|
|
115
|
-
let ready = false;
|
|
116
|
-
for (let i = 0; i < 60; i++) {
|
|
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}`);
|
|
117
255
|
try {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
} catch {
|
|
122
|
-
await new Promise(r => setTimeout(r, 500));
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (!ready) {
|
|
127
|
-
console.error('\x1b[31mError: Proxy failed to start within 30 seconds.\x1b[0m');
|
|
128
|
-
process.exit(1);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
console.log(`\x1b[32m✓ Proxy is ready on port ${port}!\x1b[0m\n`);
|
|
132
|
-
|
|
133
|
-
// Check if claude is installed
|
|
134
|
-
try {
|
|
135
|
-
execSync('which claude', { stdio: 'ignore' });
|
|
136
|
-
} catch {
|
|
137
|
-
console.error('\x1b[31mError: Claude Code CLI not found.\x1b[0m');
|
|
138
|
-
console.error('Install it with: npm install -g @anthropic-ai/claude-code');
|
|
139
|
-
process.exit(1);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Launch Claude with proxy config
|
|
143
|
-
const claudeArgs = args.slice(command === 'run' ? 1 : 0);
|
|
144
|
-
const claudeProcess = spawn('claude', claudeArgs, {
|
|
145
|
-
stdio: 'inherit',
|
|
146
|
-
env: {
|
|
147
|
-
...process.env,
|
|
148
|
-
ANTHROPIC_BASE_URL: `http://localhost:${port}`,
|
|
149
|
-
ANTHROPIC_API_KEY: 'dummy'
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
// Cleanup on exit
|
|
154
|
-
const cleanup = () => {
|
|
155
|
-
console.log('\n\x1b[33mStopping Antigravity Claude Proxy...\x1b[0m');
|
|
156
|
-
try {
|
|
157
|
-
execSync(`lsof -ti tcp:${port} | xargs kill 2>/dev/null`, { stdio: 'ignore' });
|
|
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}`);
|
|
158
259
|
} catch {}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
claudeProcess.on('close', (code) => {
|
|
163
|
-
cleanup();
|
|
164
|
-
process.exit(code || 0);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
process.on('SIGINT', () => {
|
|
168
|
-
claudeProcess.kill('SIGINT');
|
|
169
|
-
});
|
|
170
|
-
process.on('SIGTERM', () => {
|
|
171
|
-
claudeProcess.kill('SIGTERM');
|
|
172
|
-
});
|
|
260
|
+
} else {
|
|
261
|
+
console.log(`${C.yellow}✗ Proxy is not running on port ${port}${C.reset}`);
|
|
262
|
+
}
|
|
173
263
|
break;
|
|
174
264
|
}
|
|
175
265
|
|
|
@@ -198,14 +288,116 @@ async function main() {
|
|
|
198
288
|
showVersion();
|
|
199
289
|
break;
|
|
200
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
|
+
|
|
201
390
|
default:
|
|
202
|
-
console.error(
|
|
391
|
+
console.error(`${C.red}Unknown command: ${command}${C.reset}`);
|
|
203
392
|
console.error('Run "proxy-claude --help" for usage information.');
|
|
204
393
|
process.exit(1);
|
|
205
394
|
}
|
|
206
395
|
}
|
|
207
396
|
|
|
208
397
|
main().catch((err) => {
|
|
209
|
-
console.error(
|
|
398
|
+
console.error(`${C.red}Error:${C.reset}`, err.message);
|
|
399
|
+
if (process.env.DEBUG) {
|
|
400
|
+
console.error(err.stack);
|
|
401
|
+
}
|
|
210
402
|
process.exit(1);
|
|
211
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.5",
|
|
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,580 @@
|
|
|
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
|
+
// All available models in Antigravity (fetched dynamically, but this is a fallback)
|
|
54
|
+
const ALL_MODELS = [
|
|
55
|
+
// Claude models
|
|
56
|
+
{ id: 'claude-opus-4-5-thinking', family: 'claude', tier: 'opus', description: 'Claude Opus 4.5 with thinking' },
|
|
57
|
+
{ id: 'claude-sonnet-4-5-thinking', family: 'claude', tier: 'sonnet', description: 'Claude Sonnet 4.5 with thinking' },
|
|
58
|
+
{ id: 'claude-sonnet-4-5', family: 'claude', tier: 'sonnet', description: 'Claude Sonnet 4.5' },
|
|
59
|
+
// Gemini models
|
|
60
|
+
{ id: 'gemini-3-pro-high', family: 'gemini', tier: 'opus', description: 'Gemini 3 Pro High (best quality)' },
|
|
61
|
+
{ id: 'gemini-3-pro-low', family: 'gemini', tier: 'sonnet', description: 'Gemini 3 Pro Low' },
|
|
62
|
+
{ id: 'gemini-3-flash', family: 'gemini', tier: 'sonnet', description: 'Gemini 3 Flash (fast)' },
|
|
63
|
+
{ id: 'gemini-2.5-flash-lite', family: 'gemini', tier: 'haiku', description: 'Gemini 2.5 Flash Lite (fastest)' },
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
// Model tier descriptions
|
|
67
|
+
const TIER_INFO = {
|
|
68
|
+
opus: {
|
|
69
|
+
name: 'Opus (Primary)',
|
|
70
|
+
description: 'Main model for complex tasks',
|
|
71
|
+
envKey: 'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
72
|
+
},
|
|
73
|
+
sonnet: {
|
|
74
|
+
name: 'Sonnet (Default)',
|
|
75
|
+
description: 'Balanced model for most tasks',
|
|
76
|
+
envKey: 'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
77
|
+
},
|
|
78
|
+
haiku: {
|
|
79
|
+
name: 'Haiku (Fast)',
|
|
80
|
+
description: 'Quick model for simple tasks & background',
|
|
81
|
+
envKey: 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create readline interface for user input
|
|
87
|
+
*/
|
|
88
|
+
function createPrompt() {
|
|
89
|
+
return readline.createInterface({
|
|
90
|
+
input: process.stdin,
|
|
91
|
+
output: process.stdout,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Prompt user for input
|
|
97
|
+
*/
|
|
98
|
+
async function prompt(rl, question, defaultValue = '') {
|
|
99
|
+
return new Promise((resolve) => {
|
|
100
|
+
const defaultText = defaultValue ? ` ${COLORS.dim}(${defaultValue})${COLORS.reset}` : '';
|
|
101
|
+
rl.question(`${question}${defaultText}: `, (answer) => {
|
|
102
|
+
resolve(answer.trim() || defaultValue);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Prompt for yes/no
|
|
109
|
+
*/
|
|
110
|
+
async function confirm(rl, question, defaultYes = true) {
|
|
111
|
+
const hint = defaultYes ? 'Y/n' : 'y/N';
|
|
112
|
+
const answer = await prompt(rl, `${question} [${hint}]`);
|
|
113
|
+
if (!answer) return defaultYes;
|
|
114
|
+
return answer.toLowerCase().startsWith('y');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Print styled message
|
|
119
|
+
*/
|
|
120
|
+
function print(message, type = 'info') {
|
|
121
|
+
const icons = {
|
|
122
|
+
info: `${COLORS.blue}ℹ${COLORS.reset}`,
|
|
123
|
+
success: `${COLORS.green}✓${COLORS.reset}`,
|
|
124
|
+
warn: `${COLORS.yellow}⚠${COLORS.reset}`,
|
|
125
|
+
error: `${COLORS.red}✗${COLORS.reset}`,
|
|
126
|
+
step: `${COLORS.cyan}▸${COLORS.reset}`,
|
|
127
|
+
};
|
|
128
|
+
console.log(`${icons[type] || ''} ${message}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Print header
|
|
133
|
+
*/
|
|
134
|
+
function printHeader() {
|
|
135
|
+
console.log('');
|
|
136
|
+
console.log(`${COLORS.cyan}╔══════════════════════════════════════════════════════════╗${COLORS.reset}`);
|
|
137
|
+
console.log(`${COLORS.cyan}║${COLORS.reset} ${COLORS.bold}Proxy Claude - Setup Wizard${COLORS.reset} ${COLORS.cyan}║${COLORS.reset}`);
|
|
138
|
+
console.log(`${COLORS.cyan}║${COLORS.reset} ${COLORS.dim}Configure everything in one go${COLORS.reset} ${COLORS.cyan}║${COLORS.reset}`);
|
|
139
|
+
console.log(`${COLORS.cyan}╚══════════════════════════════════════════════════════════╝${COLORS.reset}`);
|
|
140
|
+
console.log('');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Print step header
|
|
145
|
+
*/
|
|
146
|
+
function printStep(step, total, title) {
|
|
147
|
+
console.log('');
|
|
148
|
+
console.log(`${COLORS.bold}${COLORS.blue}[${step}/${total}]${COLORS.reset} ${COLORS.bold}${title}${COLORS.reset}`);
|
|
149
|
+
console.log(`${COLORS.dim}${'─'.repeat(50)}${COLORS.reset}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Check if a command exists
|
|
154
|
+
*/
|
|
155
|
+
function commandExists(cmd) {
|
|
156
|
+
try {
|
|
157
|
+
const checkCmd = PLATFORM.isWindows ? `where ${cmd}` : `which ${cmd}`;
|
|
158
|
+
execSync(checkCmd, { stdio: 'ignore' });
|
|
159
|
+
return true;
|
|
160
|
+
} catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Check Node.js version
|
|
167
|
+
*/
|
|
168
|
+
function checkNodeVersion() {
|
|
169
|
+
const version = process.version;
|
|
170
|
+
const major = parseInt(version.slice(1).split('.')[0], 10);
|
|
171
|
+
return { version, major, ok: major >= 18 };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Check if Claude Code CLI is installed
|
|
176
|
+
*/
|
|
177
|
+
function checkClaudeCli() {
|
|
178
|
+
return commandExists('claude');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Install Claude Code CLI
|
|
183
|
+
*/
|
|
184
|
+
async function installClaudeCli() {
|
|
185
|
+
print('Installing Claude Code CLI...', 'step');
|
|
186
|
+
try {
|
|
187
|
+
execSync('npm install -g @anthropic-ai/claude-code', {
|
|
188
|
+
stdio: 'inherit',
|
|
189
|
+
shell: true
|
|
190
|
+
});
|
|
191
|
+
print('Claude Code CLI installed successfully', 'success');
|
|
192
|
+
return true;
|
|
193
|
+
} catch (error) {
|
|
194
|
+
print(`Failed to install Claude Code CLI: ${error.message}`, 'error');
|
|
195
|
+
print('Please install manually: npm install -g @anthropic-ai/claude-code', 'info');
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Check for existing accounts
|
|
202
|
+
*/
|
|
203
|
+
function getExistingAccounts() {
|
|
204
|
+
try {
|
|
205
|
+
if (fs.existsSync(ACCOUNTS_FILE)) {
|
|
206
|
+
const data = JSON.parse(fs.readFileSync(ACCOUNTS_FILE, 'utf-8'));
|
|
207
|
+
return data.accounts || [];
|
|
208
|
+
}
|
|
209
|
+
} catch {}
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Fetch available models from the API using account token
|
|
215
|
+
*/
|
|
216
|
+
async function fetchAvailableModels(accounts) {
|
|
217
|
+
if (!accounts || accounts.length === 0) {
|
|
218
|
+
return ALL_MODELS;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
// Get token from first account
|
|
223
|
+
const account = accounts[0];
|
|
224
|
+
const token = account.accessToken;
|
|
225
|
+
|
|
226
|
+
if (!token) {
|
|
227
|
+
return ALL_MODELS;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Import the model API
|
|
231
|
+
const { fetchAvailableModels: fetchModels } = await import('../cloudcode/model-api.js');
|
|
232
|
+
const data = await fetchModels(token);
|
|
233
|
+
|
|
234
|
+
if (data && data.models) {
|
|
235
|
+
const models = [];
|
|
236
|
+
for (const [modelId, modelData] of Object.entries(data.models)) {
|
|
237
|
+
// Only include Claude and Gemini models
|
|
238
|
+
if (!modelId.includes('claude') && !modelId.includes('gemini')) continue;
|
|
239
|
+
|
|
240
|
+
// Determine family
|
|
241
|
+
const family = modelId.includes('claude') ? 'claude' : 'gemini';
|
|
242
|
+
|
|
243
|
+
// Determine tier based on model name
|
|
244
|
+
let tier = 'sonnet';
|
|
245
|
+
if (modelId.includes('opus') || modelId.includes('pro-high')) tier = 'opus';
|
|
246
|
+
else if (modelId.includes('haiku') || modelId.includes('flash-lite')) tier = 'haiku';
|
|
247
|
+
|
|
248
|
+
// Check if model has quota (remaining > 0)
|
|
249
|
+
const hasQuota = modelData.remainingFraction === undefined || modelData.remainingFraction > 0;
|
|
250
|
+
|
|
251
|
+
models.push({
|
|
252
|
+
id: modelId,
|
|
253
|
+
family,
|
|
254
|
+
tier,
|
|
255
|
+
description: modelData.displayName || modelId,
|
|
256
|
+
hasQuota,
|
|
257
|
+
remainingFraction: modelData.remainingFraction,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (models.length > 0) {
|
|
262
|
+
return models;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
} catch (error) {
|
|
266
|
+
print(`Could not fetch models from API: ${error.message}`, 'warn');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return ALL_MODELS;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Add Google account via OAuth
|
|
274
|
+
*/
|
|
275
|
+
async function addGoogleAccount() {
|
|
276
|
+
print('Starting Google OAuth flow...', 'step');
|
|
277
|
+
print('A browser window will open for authentication.', 'info');
|
|
278
|
+
console.log('');
|
|
279
|
+
|
|
280
|
+
return new Promise((resolve) => {
|
|
281
|
+
const accountsScript = path.join(__dirname, 'accounts.js');
|
|
282
|
+
const child = spawn('node', [accountsScript, 'add'], {
|
|
283
|
+
stdio: 'inherit',
|
|
284
|
+
shell: true,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
child.on('close', (code) => {
|
|
288
|
+
resolve(code === 0);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Load existing Claude settings
|
|
295
|
+
*/
|
|
296
|
+
function loadClaudeSettings() {
|
|
297
|
+
try {
|
|
298
|
+
if (fs.existsSync(CLAUDE_SETTINGS_FILE)) {
|
|
299
|
+
return JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_FILE, 'utf-8'));
|
|
300
|
+
}
|
|
301
|
+
} catch {}
|
|
302
|
+
return {};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Save Claude settings
|
|
307
|
+
*/
|
|
308
|
+
function saveClaudeSettings(settings) {
|
|
309
|
+
// Ensure directory exists
|
|
310
|
+
if (!fs.existsSync(CLAUDE_CONFIG_DIR)) {
|
|
311
|
+
fs.mkdirSync(CLAUDE_CONFIG_DIR, { recursive: true });
|
|
312
|
+
}
|
|
313
|
+
fs.writeFileSync(CLAUDE_SETTINGS_FILE, JSON.stringify(settings, null, 2));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Select a model for a specific tier
|
|
318
|
+
*/
|
|
319
|
+
async function selectModelForTier(rl, tier, availableModels) {
|
|
320
|
+
const tierInfo = TIER_INFO[tier];
|
|
321
|
+
console.log('');
|
|
322
|
+
console.log(`${COLORS.bold}${tierInfo.name}${COLORS.reset} - ${COLORS.dim}${tierInfo.description}${COLORS.reset}`);
|
|
323
|
+
console.log('');
|
|
324
|
+
|
|
325
|
+
// Filter models that are suitable for this tier or higher
|
|
326
|
+
// opus can use any model, sonnet can use sonnet/haiku, haiku uses haiku
|
|
327
|
+
const tierPriority = { opus: 3, sonnet: 2, haiku: 1 };
|
|
328
|
+
const minTier = tierPriority[tier];
|
|
329
|
+
|
|
330
|
+
const suitableModels = availableModels.filter(m => {
|
|
331
|
+
const modelTier = tierPriority[m.tier] || 2;
|
|
332
|
+
return modelTier >= minTier - 1; // Allow one tier lower
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Sort: models with quota first, then by family (claude first), then by tier
|
|
336
|
+
suitableModels.sort((a, b) => {
|
|
337
|
+
// Quota status
|
|
338
|
+
if (a.hasQuota !== b.hasQuota) return a.hasQuota ? -1 : 1;
|
|
339
|
+
// Family (claude first for opus/sonnet, gemini first for haiku)
|
|
340
|
+
if (tier === 'haiku') {
|
|
341
|
+
if (a.family !== b.family) return a.family === 'gemini' ? -1 : 1;
|
|
342
|
+
} else {
|
|
343
|
+
if (a.family !== b.family) return a.family === 'claude' ? -1 : 1;
|
|
344
|
+
}
|
|
345
|
+
// Tier priority
|
|
346
|
+
return (tierPriority[b.tier] || 0) - (tierPriority[a.tier] || 0);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// Display models
|
|
350
|
+
console.log(`${COLORS.dim}Available models:${COLORS.reset}`);
|
|
351
|
+
suitableModels.forEach((model, index) => {
|
|
352
|
+
const quotaStatus = model.hasQuota === false
|
|
353
|
+
? `${COLORS.red}(no quota)${COLORS.reset}`
|
|
354
|
+
: model.remainingFraction !== undefined
|
|
355
|
+
? `${COLORS.green}(${Math.round(model.remainingFraction * 100)}% quota)${COLORS.reset}`
|
|
356
|
+
: '';
|
|
357
|
+
const familyBadge = model.family === 'claude'
|
|
358
|
+
? `${COLORS.magenta}[Claude]${COLORS.reset}`
|
|
359
|
+
: `${COLORS.blue}[Gemini]${COLORS.reset}`;
|
|
360
|
+
console.log(` ${COLORS.cyan}${index + 1}.${COLORS.reset} ${model.id} ${familyBadge} ${quotaStatus}`);
|
|
361
|
+
});
|
|
362
|
+
console.log('');
|
|
363
|
+
|
|
364
|
+
// Default to first model with quota
|
|
365
|
+
const defaultIndex = suitableModels.findIndex(m => m.hasQuota !== false) + 1 || 1;
|
|
366
|
+
|
|
367
|
+
const choice = await prompt(rl, `Select model for ${tier} (1-${suitableModels.length})`, String(defaultIndex));
|
|
368
|
+
const index = parseInt(choice, 10) - 1;
|
|
369
|
+
|
|
370
|
+
if (index >= 0 && index < suitableModels.length) {
|
|
371
|
+
return suitableModels[index].id;
|
|
372
|
+
}
|
|
373
|
+
return suitableModels[0]?.id || ALL_MODELS.find(m => m.tier === tier)?.id;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Configure Claude Code settings with selected models
|
|
378
|
+
*/
|
|
379
|
+
function configureClaudeSettingsWithModels(modelConfig, port = 8080) {
|
|
380
|
+
const existing = loadClaudeSettings();
|
|
381
|
+
|
|
382
|
+
const newSettings = {
|
|
383
|
+
...existing,
|
|
384
|
+
env: {
|
|
385
|
+
...(existing.env || {}),
|
|
386
|
+
ANTHROPIC_AUTH_TOKEN: 'proxy-claude',
|
|
387
|
+
ANTHROPIC_BASE_URL: `http://localhost:${port}`,
|
|
388
|
+
ANTHROPIC_MODEL: modelConfig.sonnet, // Default model is sonnet
|
|
389
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: modelConfig.opus,
|
|
390
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: modelConfig.sonnet,
|
|
391
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: modelConfig.haiku,
|
|
392
|
+
CLAUDE_CODE_SUBAGENT_MODEL: modelConfig.haiku, // Subagent uses haiku for efficiency
|
|
393
|
+
ENABLE_EXPERIMENTAL_MCP_CLI: 'true',
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
// Also set hasCompletedOnboarding to skip Claude's own onboarding
|
|
398
|
+
if (!existing.hasCompletedOnboarding) {
|
|
399
|
+
newSettings.hasCompletedOnboarding = true;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
saveClaudeSettings(newSettings);
|
|
403
|
+
return newSettings;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Print summary with selected models
|
|
408
|
+
*/
|
|
409
|
+
function printSummary(accounts, modelConfig, port) {
|
|
410
|
+
console.log('');
|
|
411
|
+
console.log(`${COLORS.green}╔══════════════════════════════════════════════════════════╗${COLORS.reset}`);
|
|
412
|
+
console.log(`${COLORS.green}║${COLORS.reset} ${COLORS.bold}${COLORS.green}Setup Complete!${COLORS.reset} ${COLORS.green}║${COLORS.reset}`);
|
|
413
|
+
console.log(`${COLORS.green}╚══════════════════════════════════════════════════════════╝${COLORS.reset}`);
|
|
414
|
+
console.log('');
|
|
415
|
+
|
|
416
|
+
console.log(`${COLORS.bold}Configuration Summary:${COLORS.reset}`);
|
|
417
|
+
console.log(` ${COLORS.dim}•${COLORS.reset} Accounts: ${accounts.length} configured`);
|
|
418
|
+
console.log(` ${COLORS.dim}•${COLORS.reset} Opus model: ${COLORS.cyan}${modelConfig.opus}${COLORS.reset}`);
|
|
419
|
+
console.log(` ${COLORS.dim}•${COLORS.reset} Sonnet model: ${COLORS.cyan}${modelConfig.sonnet}${COLORS.reset}`);
|
|
420
|
+
console.log(` ${COLORS.dim}•${COLORS.reset} Haiku model: ${COLORS.cyan}${modelConfig.haiku}${COLORS.reset}`);
|
|
421
|
+
console.log(` ${COLORS.dim}•${COLORS.reset} Proxy port: ${port}`);
|
|
422
|
+
console.log(` ${COLORS.dim}•${COLORS.reset} Settings saved to: ${CLAUDE_SETTINGS_FILE}`);
|
|
423
|
+
console.log('');
|
|
424
|
+
|
|
425
|
+
console.log(`${COLORS.bold}To start using Claude with the proxy:${COLORS.reset}`);
|
|
426
|
+
console.log('');
|
|
427
|
+
console.log(` ${COLORS.cyan}proxy-claude${COLORS.reset}`);
|
|
428
|
+
console.log('');
|
|
429
|
+
console.log(`${COLORS.dim}Or start components separately:${COLORS.reset}`);
|
|
430
|
+
console.log(` ${COLORS.dim}proxy-claude start${COLORS.reset} - Start proxy server only`);
|
|
431
|
+
console.log(` ${COLORS.dim}proxy-claude web${COLORS.reset} - Start with web dashboard`);
|
|
432
|
+
console.log('');
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Main onboarding flow
|
|
437
|
+
*/
|
|
438
|
+
export async function runOnboarding(options = {}) {
|
|
439
|
+
const { skipIfConfigured = false, quiet = false } = options;
|
|
440
|
+
|
|
441
|
+
// Check if already configured
|
|
442
|
+
const existingAccounts = getExistingAccounts();
|
|
443
|
+
const existingSettings = loadClaudeSettings();
|
|
444
|
+
const isConfigured = existingAccounts.length > 0 && existingSettings.env?.ANTHROPIC_BASE_URL;
|
|
445
|
+
|
|
446
|
+
if (skipIfConfigured && isConfigured) {
|
|
447
|
+
if (!quiet) {
|
|
448
|
+
print('Already configured. Use --force to reconfigure.', 'info');
|
|
449
|
+
}
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
printHeader();
|
|
454
|
+
|
|
455
|
+
const rl = createPrompt();
|
|
456
|
+
const totalSteps = 4;
|
|
457
|
+
let currentStep = 0;
|
|
458
|
+
|
|
459
|
+
try {
|
|
460
|
+
// Step 1: Check prerequisites
|
|
461
|
+
printStep(++currentStep, totalSteps, 'Checking Prerequisites');
|
|
462
|
+
|
|
463
|
+
// Check Node.js
|
|
464
|
+
const nodeCheck = checkNodeVersion();
|
|
465
|
+
if (nodeCheck.ok) {
|
|
466
|
+
print(`Node.js ${nodeCheck.version} ✓`, 'success');
|
|
467
|
+
} else {
|
|
468
|
+
print(`Node.js ${nodeCheck.version} - version 18+ required`, 'error');
|
|
469
|
+
rl.close();
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Check/Install Claude CLI
|
|
474
|
+
if (checkClaudeCli()) {
|
|
475
|
+
print('Claude Code CLI ✓', 'success');
|
|
476
|
+
} else {
|
|
477
|
+
print('Claude Code CLI not found', 'warn');
|
|
478
|
+
const install = await confirm(rl, 'Install Claude Code CLI now?');
|
|
479
|
+
if (install) {
|
|
480
|
+
const installed = await installClaudeCli();
|
|
481
|
+
if (!installed) {
|
|
482
|
+
rl.close();
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
} else {
|
|
486
|
+
print('Claude Code CLI is required. Please install it first.', 'error');
|
|
487
|
+
rl.close();
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Step 2: Account Setup
|
|
493
|
+
printStep(++currentStep, totalSteps, 'Account Configuration');
|
|
494
|
+
|
|
495
|
+
if (existingAccounts.length > 0) {
|
|
496
|
+
print(`Found ${existingAccounts.length} existing account(s):`, 'info');
|
|
497
|
+
existingAccounts.forEach((acc, i) => {
|
|
498
|
+
console.log(` ${COLORS.dim}${i + 1}.${COLORS.reset} ${acc.email}`);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
const addMore = await confirm(rl, 'Add another account?', false);
|
|
502
|
+
if (addMore) {
|
|
503
|
+
await addGoogleAccount();
|
|
504
|
+
}
|
|
505
|
+
} else {
|
|
506
|
+
print('No accounts configured yet.', 'info');
|
|
507
|
+
const addAccount = await confirm(rl, 'Add a Google account now?');
|
|
508
|
+
if (addAccount) {
|
|
509
|
+
await addGoogleAccount();
|
|
510
|
+
} else {
|
|
511
|
+
print('You can add accounts later with: proxy-claude accounts add', 'info');
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Reload accounts after potential additions
|
|
516
|
+
const accounts = getExistingAccounts();
|
|
517
|
+
|
|
518
|
+
// Step 3: Model Configuration
|
|
519
|
+
printStep(++currentStep, totalSteps, 'Model Configuration');
|
|
520
|
+
|
|
521
|
+
print('Fetching available models from your account...', 'step');
|
|
522
|
+
const availableModels = await fetchAvailableModels(accounts);
|
|
523
|
+
print(`Found ${availableModels.length} models available`, 'success');
|
|
524
|
+
|
|
525
|
+
console.log('');
|
|
526
|
+
print('Now select a model for each tier. Models with quota are shown first.', 'info');
|
|
527
|
+
print(`${COLORS.dim}Tip: Use Gemini for Haiku to save your Claude quota!${COLORS.reset}`, 'info');
|
|
528
|
+
|
|
529
|
+
// Select model for each tier
|
|
530
|
+
const modelConfig = {
|
|
531
|
+
opus: await selectModelForTier(rl, 'opus', availableModels),
|
|
532
|
+
sonnet: await selectModelForTier(rl, 'sonnet', availableModels),
|
|
533
|
+
haiku: await selectModelForTier(rl, 'haiku', availableModels),
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
console.log('');
|
|
537
|
+
print('Model configuration:', 'success');
|
|
538
|
+
console.log(` ${COLORS.dim}Opus:${COLORS.reset} ${COLORS.cyan}${modelConfig.opus}${COLORS.reset}`);
|
|
539
|
+
console.log(` ${COLORS.dim}Sonnet:${COLORS.reset} ${COLORS.cyan}${modelConfig.sonnet}${COLORS.reset}`);
|
|
540
|
+
console.log(` ${COLORS.dim}Haiku:${COLORS.reset} ${COLORS.cyan}${modelConfig.haiku}${COLORS.reset}`);
|
|
541
|
+
|
|
542
|
+
// Step 4: Apply Configuration
|
|
543
|
+
printStep(++currentStep, totalSteps, 'Applying Configuration');
|
|
544
|
+
|
|
545
|
+
const port = parseInt(process.env.PORT || '8080', 10);
|
|
546
|
+
|
|
547
|
+
// Configure Claude Code settings
|
|
548
|
+
print('Configuring Claude Code settings...', 'step');
|
|
549
|
+
configureClaudeSettingsWithModels(modelConfig, port);
|
|
550
|
+
print(`Settings saved to ${CLAUDE_SETTINGS_FILE}`, 'success');
|
|
551
|
+
|
|
552
|
+
// Print summary
|
|
553
|
+
printSummary(accounts, modelConfig, port);
|
|
554
|
+
|
|
555
|
+
rl.close();
|
|
556
|
+
return true;
|
|
557
|
+
|
|
558
|
+
} catch (error) {
|
|
559
|
+
print(`Setup error: ${error.message}`, 'error');
|
|
560
|
+
rl.close();
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Check if first run (no config exists)
|
|
567
|
+
*/
|
|
568
|
+
export function isFirstRun() {
|
|
569
|
+
const accounts = getExistingAccounts();
|
|
570
|
+
const settings = loadClaudeSettings();
|
|
571
|
+
return accounts.length === 0 || !settings.env?.ANTHROPIC_BASE_URL;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// CLI entry point
|
|
575
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
576
|
+
const force = process.argv.includes('--force') || process.argv.includes('-f');
|
|
577
|
+
runOnboarding({ skipIfConfigured: !force }).then((success) => {
|
|
578
|
+
process.exit(success ? 0 : 1);
|
|
579
|
+
});
|
|
580
|
+
}
|