@kamel-ahmed/proxy-claude 1.0.3 → 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.
Files changed (3) hide show
  1. package/bin/cli.js +301 -109
  2. package/package.json +2 -2
  3. 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,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 (interactive)
29
- run Same as default - start proxy + Claude Code
30
- start Start the proxy server only (foreground)
31
- web Same as start - server with web dashboard
32
- accounts Manage Google accounts (interactive)
33
- accounts add Add a new Google account via OAuth
34
- accounts list List all configured accounts
35
- accounts remove Remove accounts interactively
36
- accounts verify Verify account tokens are valid
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 run # Same as above
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
- CONFIGURATION:
56
- Claude Code CLI (~/.claude/settings.json):
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 'setup':
84
- 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);
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 'web':
93
- // Alias for start (server with web dashboard)
94
- await import('../src/index.js');
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 undefined:
98
- case 'run': {
99
- // Default: Start proxy in background + launch Claude Code
100
- const { spawn, execSync } = await import('child_process');
101
- const port = process.env.PORT || 8080;
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
- execSync(`curl -s http://localhost:${port}/health`, { stdio: 'ignore' });
119
- ready = true;
120
- break;
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
- console.log('\x1b[32m✓ Proxy stopped\x1b[0m');
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(`Unknown command: ${command}`);
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('Error:', err.message);
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",
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
+ }