@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 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 <command> [options]
26
-
27
- COMMANDS:
28
- start Start the proxy server (default port: 8080)
29
- accounts Manage Google accounts (interactive)
30
- accounts add Add a new Google account via OAuth
31
- accounts list List all configured accounts
32
- accounts remove Remove accounts interactively
33
- accounts verify Verify account tokens are valid
34
- accounts clear Remove all accounts
35
- refresh Check and refresh account tokens
36
- setup Install Claude Code CLI and create global 'proxy-claude' command
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 start
47
- PORT=3000 proxy-claude start
48
- proxy-claude accounts add
49
- proxy-claude accounts list
50
-
51
- CONFIGURATION:
52
- Claude Code CLI (~/.claude/settings.json):
53
- {
54
- "env": {
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 'setup':
80
- await import('../src/cli/setup.js').then(module => module.runSetup());
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 undefined:
85
- // Default to starting the server
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(`Unknown command: ${command}`);
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('Error:', err.message);
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.2",
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 setup",
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
+ }