hedgequantx 2.7.82 → 2.7.83

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "2.7.82",
3
+ "version": "2.7.83",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -104,6 +104,22 @@ const activateProvider = (config, providerId, data) => {
104
104
  Object.assign(config.providers[providerId], data, { active: true, configuredAt: new Date().toISOString() });
105
105
  };
106
106
 
107
+ /** Wait for child process to exit */
108
+ const waitForProcessExit = (childProcess, timeoutMs = 15000, intervalMs = 500) => {
109
+ return new Promise((resolve) => {
110
+ if (!childProcess) return resolve();
111
+ let elapsed = 0;
112
+ const checkInterval = setInterval(() => {
113
+ elapsed += intervalMs;
114
+ if (childProcess.exitCode !== null || childProcess.killed || elapsed >= timeoutMs) {
115
+ clearInterval(checkInterval);
116
+ if (elapsed >= timeoutMs) try { childProcess.kill(); } catch (e) { /* ignore */ }
117
+ resolve();
118
+ }
119
+ }, intervalMs);
120
+ });
121
+ };
122
+
107
123
  /** Handle CLIProxy connection (with auto-install) */
108
124
  const handleCliProxyConnection = async (provider, config, boxWidth) => {
109
125
  console.log();
@@ -160,8 +176,6 @@ const handleCliProxyConnection = async (provider, config, boxWidth) => {
160
176
 
161
177
  const existingModels = await cliproxy.fetchProviderModels(provider.id);
162
178
 
163
- // Debug output
164
- console.log(chalk.gray(` > RESULT: success=${existingModels.success}, models=${existingModels.models?.length || 0}, error=${existingModels.error || 'none'}`));
165
179
 
166
180
  if (existingModels.success && existingModels.models.length > 0) {
167
181
  // Models already available - skip OAuth, go directly to model selection
@@ -205,74 +219,67 @@ const handleCliProxyConnection = async (provider, config, boxWidth) => {
205
219
  console.log(chalk.cyan('\n OPEN THIS URL IN YOUR BROWSER TO AUTHENTICATE:\n'));
206
220
  console.log(chalk.yellow(` ${loginResult.url}\n`));
207
221
 
222
+ // Get callback port for this provider
223
+ const callbackPort = cliproxy.getCallbackPort(provider.id);
224
+ const isPollingAuth = (provider.id === 'qwen'); // Qwen uses polling, not callback
225
+
208
226
  // Different flow for VPS/headless vs local
209
227
  if (loginResult.isHeadless) {
210
228
  console.log(chalk.magenta(' ══════════════════════════════════════════════════════════'));
211
229
  console.log(chalk.magenta(' VPS/SSH DETECTED - MANUAL CALLBACK REQUIRED'));
212
230
  console.log(chalk.magenta(' ══════════════════════════════════════════════════════════\n'));
213
- console.log(chalk.white(' 1. OPEN THE URL ABOVE IN YOUR LOCAL BROWSER'));
214
- console.log(chalk.white(' 2. AUTHORIZE THE APPLICATION'));
215
- console.log(chalk.white(' 3. YOU WILL SEE A BLANK PAGE - THIS IS NORMAL'));
216
- console.log(chalk.white(' 4. COPY THE FULL URL FROM YOUR BROWSER ADDRESS BAR'));
217
- console.log(chalk.white(' (IT STARTS WITH: http://localhost:' + cliproxy.CALLBACK_PORT + '/...)'));
218
- console.log(chalk.white(' 5. PASTE IT BELOW:\n'));
219
-
220
- const callbackUrl = await prompts.textInput(chalk.cyan(' CALLBACK URL: '));
221
231
 
222
- if (!callbackUrl || !callbackUrl.includes('localhost')) {
223
- console.log(chalk.red('\n INVALID CALLBACK URL'));
224
- if (loginResult.childProcess) loginResult.childProcess.kill();
232
+ if (isPollingAuth) {
233
+ // Qwen uses polling - just wait for user to authorize
234
+ console.log(chalk.white(' 1. OPEN THE URL ABOVE IN YOUR BROWSER'));
235
+ console.log(chalk.white(' 2. AUTHORIZE THE APPLICATION'));
236
+ console.log(chalk.white(' 3. WAIT FOR AUTHENTICATION TO COMPLETE'));
237
+ console.log(chalk.white(' 4. PRESS ENTER WHEN DONE\n'));
225
238
  await prompts.waitForEnter();
226
- return false;
227
- }
228
-
229
- // Process the callback - send to the login process listening on CALLBACK_PORT
230
- const spinner = ora({ text: 'PROCESSING CALLBACK...', color: 'yellow' }).start();
231
-
232
- try {
233
- const callbackResult = await cliproxy.processCallback(callbackUrl.trim());
234
239
 
235
- if (!callbackResult.success) {
236
- spinner.fail(`CALLBACK FAILED: ${callbackResult.error}`);
240
+ const spinner = ora({ text: 'WAITING FOR AUTHENTICATION...', color: 'yellow' }).start();
241
+ await waitForProcessExit(loginResult.childProcess, 90000, 1000);
242
+ spinner.succeed('AUTHENTICATION COMPLETED!');
243
+ } else {
244
+ // Standard OAuth with callback
245
+ console.log(chalk.white(' 1. OPEN THE URL ABOVE IN YOUR LOCAL BROWSER'));
246
+ console.log(chalk.white(' 2. AUTHORIZE THE APPLICATION'));
247
+ console.log(chalk.white(' 3. YOU WILL SEE A BLANK PAGE - THIS IS NORMAL'));
248
+ console.log(chalk.white(' 4. COPY THE FULL URL FROM YOUR BROWSER ADDRESS BAR'));
249
+ console.log(chalk.white(` (IT STARTS WITH: http://localhost:${callbackPort}/...)`));
250
+ console.log(chalk.white(' 5. PASTE IT BELOW:\n'));
251
+
252
+ const callbackUrl = await prompts.textInput(chalk.cyan(' CALLBACK URL: '));
253
+
254
+ if (!callbackUrl || !callbackUrl.includes('localhost')) {
255
+ console.log(chalk.red('\n INVALID CALLBACK URL'));
237
256
  if (loginResult.childProcess) loginResult.childProcess.kill();
238
257
  await prompts.waitForEnter();
239
258
  return false;
240
259
  }
241
260
 
242
- spinner.text = 'EXCHANGING TOKEN...';
261
+ // Process the callback - send to the login process
262
+ const spinner = ora({ text: 'PROCESSING CALLBACK...', color: 'yellow' }).start();
243
263
 
244
- // Wait for login process to exit naturally (it saves auth file then exits)
245
- // Process typically exits 1-2 seconds after callback
246
- await new Promise((resolve) => {
247
- if (!loginResult.childProcess) return resolve();
264
+ try {
265
+ const callbackResult = await cliproxy.processCallback(callbackUrl.trim(), provider.id);
248
266
 
249
- // Check every 500ms if process is still running, max 15s
250
- let elapsed = 0;
251
- const checkInterval = setInterval(() => {
252
- elapsed += 500;
253
-
254
- // Check if process exited (exitCode is set when process exits)
255
- if (loginResult.childProcess.exitCode !== null || loginResult.childProcess.killed) {
256
- clearInterval(checkInterval);
257
- resolve();
258
- return;
259
- }
260
-
261
- // Safety timeout after 15s
262
- if (elapsed >= 15000) {
263
- clearInterval(checkInterval);
264
- try { loginResult.childProcess.kill(); } catch (e) { /* ignore */ }
265
- resolve();
266
- }
267
- }, 500);
268
- });
269
-
270
- spinner.succeed('AUTHENTICATION SUCCESSFUL!');
271
- } catch (err) {
272
- spinner.fail(`ERROR: ${err.message}`);
273
- if (loginResult.childProcess) loginResult.childProcess.kill();
274
- await prompts.waitForEnter();
275
- return false;
267
+ if (!callbackResult.success) {
268
+ spinner.fail(`CALLBACK FAILED: ${callbackResult.error}`);
269
+ if (loginResult.childProcess) loginResult.childProcess.kill();
270
+ await prompts.waitForEnter();
271
+ return false;
272
+ }
273
+
274
+ spinner.text = 'EXCHANGING TOKEN...';
275
+ await waitForProcessExit(loginResult.childProcess);
276
+ spinner.succeed('AUTHENTICATION SUCCESSFUL!');
277
+ } catch (err) {
278
+ spinner.fail(`ERROR: ${err.message}`);
279
+ if (loginResult.childProcess) loginResult.childProcess.kill();
280
+ await prompts.waitForEnter();
281
+ return false;
282
+ }
276
283
  }
277
284
 
278
285
  } else {
@@ -280,31 +287,8 @@ const handleCliProxyConnection = async (provider, config, boxWidth) => {
280
287
  console.log(chalk.gray(' AFTER AUTHENTICATING IN YOUR BROWSER, PRESS ENTER...'));
281
288
  await prompts.waitForEnter();
282
289
 
283
- // Wait for login process to finish saving auth file
284
290
  const spinner = ora({ text: 'SAVING AUTHENTICATION...', color: 'yellow' }).start();
285
-
286
- await new Promise((resolve) => {
287
- if (!loginResult.childProcess) return resolve();
288
-
289
- let elapsed = 0;
290
- const checkInterval = setInterval(() => {
291
- elapsed += 500;
292
-
293
- if (loginResult.childProcess.exitCode !== null || loginResult.childProcess.killed) {
294
- clearInterval(checkInterval);
295
- resolve();
296
- return;
297
- }
298
-
299
- // Safety timeout after 15s
300
- if (elapsed >= 15000) {
301
- clearInterval(checkInterval);
302
- try { loginResult.childProcess.kill(); } catch (e) { /* ignore */ }
303
- resolve();
304
- }
305
- }, 500);
306
- });
307
-
291
+ await waitForProcessExit(loginResult.childProcess);
308
292
  spinner.succeed('AUTHENTICATION SAVED');
309
293
  }
310
294
 
@@ -14,7 +14,8 @@ const {
14
14
  INSTALL_DIR,
15
15
  AUTH_DIR,
16
16
  DEFAULT_PORT,
17
- CALLBACK_PORT,
17
+ CALLBACK_PORTS,
18
+ CALLBACK_PATHS,
18
19
  isInstalled,
19
20
  isHeadless,
20
21
  install,
@@ -23,6 +24,7 @@ const {
23
24
  stop,
24
25
  ensureRunning,
25
26
  getLoginUrl,
27
+ getCallbackPort,
26
28
  processCallback
27
29
  } = manager;
28
30
 
@@ -178,7 +180,8 @@ module.exports = {
178
180
  INSTALL_DIR,
179
181
  AUTH_DIR,
180
182
  DEFAULT_PORT,
181
- CALLBACK_PORT,
183
+ CALLBACK_PORTS,
184
+ CALLBACK_PATHS,
182
185
  isInstalled,
183
186
  isHeadless,
184
187
  install,
@@ -187,6 +190,7 @@ module.exports = {
187
190
  stop,
188
191
  ensureRunning,
189
192
  getLoginUrl,
193
+ getCallbackPort,
190
194
  processCallback,
191
195
 
192
196
  // API
@@ -26,104 +26,59 @@ const AUTH_DIR = path.join(os.homedir(), '.cli-proxy-api');
26
26
 
27
27
  // Default port
28
28
  const DEFAULT_PORT = 8317;
29
- const CALLBACK_PORT = 54545;
30
29
 
31
- /**
32
- * Detect if running in headless/VPS environment (no browser access)
33
- * @returns {boolean}
34
- */
30
+ // OAuth callback ports per provider (from CLIProxyAPI)
31
+ const CALLBACK_PORTS = {
32
+ anthropic: 54545, // Claude: /callback
33
+ openai: 1455, // Codex: /auth/callback
34
+ google: 8085, // Gemini: /oauth2callback
35
+ qwen: null, // Qwen uses polling, no callback
36
+ antigravity: 51121, // Antigravity: /oauth-callback
37
+ iflow: 11451 // iFlow: /oauth2callback
38
+ };
39
+
40
+ // OAuth callback paths per provider
41
+ const CALLBACK_PATHS = {
42
+ anthropic: '/callback',
43
+ openai: '/auth/callback',
44
+ google: '/oauth2callback',
45
+ qwen: null,
46
+ antigravity: '/oauth-callback',
47
+ iflow: '/oauth2callback'
48
+ };
49
+
50
+ /** Detect if running in headless/VPS environment (no browser access) */
35
51
  const isHeadless = () => {
36
- // 1. SSH connection = definitely VPS/remote
37
- if (process.env.SSH_CLIENT || process.env.SSH_TTY || process.env.SSH_CONNECTION) {
38
- return true;
39
- }
40
-
41
- // 2. Docker/container environment
42
- if (process.env.DOCKER_CONTAINER || process.env.KUBERNETES_SERVICE_HOST) {
43
- return true;
44
- }
45
-
46
- // 3. Common CI/CD environments
47
- if (process.env.CI || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI) {
48
- return true;
49
- }
50
-
51
- // 4. Check for display on Linux
52
+ // SSH/Docker/CI = headless
53
+ if (process.env.SSH_CLIENT || process.env.SSH_TTY || process.env.SSH_CONNECTION) return true;
54
+ if (process.env.DOCKER_CONTAINER || process.env.KUBERNETES_SERVICE_HOST) return true;
55
+ if (process.env.CI || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI) return true;
56
+ // Linux without display = headless
52
57
  if (process.platform === 'linux') {
53
- // Has display = local with GUI
54
- if (process.env.DISPLAY || process.env.WAYLAND_DISPLAY) {
55
- return false;
56
- }
57
- // No display on Linux = likely headless/VPS
58
- return true;
59
- }
60
-
61
- // 5. macOS - check if running in terminal with GUI access
62
- if (process.platform === 'darwin') {
63
- // macOS always has GUI unless SSH (checked above)
64
- return false;
65
- }
66
-
67
- // 6. Windows - usually has GUI
68
- if (process.platform === 'win32') {
69
- return false;
58
+ return !(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
70
59
  }
71
-
72
- // Default: assume local
60
+ // macOS/Windows = local with GUI
73
61
  return false;
74
62
  };
75
63
 
76
- /**
77
- * Get download URL for current platform
78
- * @returns {Object} { url, filename } or null if unsupported
79
- */
64
+ /** Get download URL for current platform */
80
65
  const getDownloadUrl = () => {
81
- const platform = process.platform;
82
- const arch = process.arch;
66
+ const platform = process.platform, arch = process.arch;
67
+ const osMap = { darwin: 'darwin', linux: 'linux', win32: 'windows' };
68
+ const extMap = { darwin: 'tar.gz', linux: 'tar.gz', win32: 'zip' };
69
+ const archMap = { x64: 'amd64', amd64: 'amd64', arm64: 'arm64' };
83
70
 
84
- let osName, archName, ext;
85
-
86
- if (platform === 'darwin') {
87
- osName = 'darwin';
88
- ext = 'tar.gz';
89
- } else if (platform === 'linux') {
90
- osName = 'linux';
91
- ext = 'tar.gz';
92
- } else if (platform === 'win32') {
93
- osName = 'windows';
94
- ext = 'zip';
95
- } else {
96
- return null;
97
- }
98
-
99
- if (arch === 'x64' || arch === 'amd64') {
100
- archName = 'amd64';
101
- } else if (arch === 'arm64') {
102
- archName = 'arm64';
103
- } else {
104
- return null;
105
- }
71
+ const osName = osMap[platform], ext = extMap[platform], archName = archMap[arch];
72
+ if (!osName || !archName) return null;
106
73
 
107
74
  const filename = `CLIProxyAPI_${CLIPROXY_VERSION}_${osName}_${archName}.${ext}`;
108
- const url = `${GITHUB_RELEASE_BASE}/v${CLIPROXY_VERSION}/${filename}`;
109
-
110
- return { url, filename, ext };
111
- };
112
-
113
- /**
114
- * Check if CLIProxyAPI is installed
115
- * @returns {boolean}
116
- */
117
- const isInstalled = () => {
118
- return fs.existsSync(BINARY_PATH);
75
+ return { url: `${GITHUB_RELEASE_BASE}/v${CLIPROXY_VERSION}/${filename}`, filename, ext };
119
76
  };
120
77
 
78
+ /** Check if CLIProxyAPI is installed */
79
+ const isInstalled = () => fs.existsSync(BINARY_PATH);
121
80
 
122
- /**
123
- * Install CLIProxyAPI
124
- * @param {Function} onProgress - Progress callback (message, percent)
125
- * @returns {Promise<Object>} { success, error }
126
- */
81
+ /** Install CLIProxyAPI */
127
82
  const install = async (onProgress = null) => {
128
83
  try {
129
84
  const download = getDownloadUrl();
@@ -175,61 +130,39 @@ const install = async (onProgress = null) => {
175
130
  }
176
131
  };
177
132
 
178
- /**
179
- * Check if CLIProxyAPI is running
180
- * @returns {Promise<Object>} { running, pid }
181
- */
133
+ /** Check if CLIProxyAPI is running */
182
134
  const isRunning = async () => {
183
- // Check PID file
184
135
  if (fs.existsSync(PID_FILE)) {
185
136
  try {
186
137
  const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim(), 10);
187
- // Check if process exists
188
138
  process.kill(pid, 0);
189
139
  return { running: true, pid };
190
- } catch (e) {
191
- // Process doesn't exist, clean up PID file
192
- fs.unlinkSync(PID_FILE);
193
- }
140
+ } catch (e) { fs.unlinkSync(PID_FILE); }
194
141
  }
195
-
196
- // Also check by trying to connect (accept 200, 401, 403 as "running")
197
142
  return new Promise((resolve) => {
198
143
  const req = http.get(`http://127.0.0.1:${DEFAULT_PORT}/v1/models`, (res) => {
199
- const running = res.statusCode === 200 || res.statusCode === 401 || res.statusCode === 403;
200
- resolve({ running, pid: null });
144
+ resolve({ running: [200, 401, 403].includes(res.statusCode), pid: null });
201
145
  });
202
146
  req.on('error', () => resolve({ running: false, pid: null }));
203
- req.setTimeout(2000, () => {
204
- req.destroy();
205
- resolve({ running: false, pid: null });
206
- });
147
+ req.setTimeout(2000, () => { req.destroy(); resolve({ running: false, pid: null }); });
207
148
  });
208
149
  };
209
150
 
210
- // Config file path
211
151
  const CONFIG_PATH = path.join(INSTALL_DIR, 'config.yaml');
212
152
 
213
- /**
214
- * Create or update config file
215
- */
153
+ /** Create or update config file */
216
154
  const ensureConfig = () => {
217
- // Always write config to ensure auth-dir is correct
218
- const config = `# HQX CLIProxyAPI Config
155
+ fs.writeFileSync(CONFIG_PATH, `# HQX CLIProxyAPI Config
219
156
  host: "127.0.0.1"
220
157
  port: ${DEFAULT_PORT}
221
158
  auth-dir: "${AUTH_DIR}"
222
159
  debug: false
223
160
  api-keys:
224
161
  - "hqx-internal-key"
225
- `;
226
- fs.writeFileSync(CONFIG_PATH, config);
162
+ `);
227
163
  };
228
164
 
229
- /**
230
- * Start CLIProxyAPI
231
- * @returns {Promise<Object>} { success, error, pid }
232
- */
165
+ /** Start CLIProxyAPI */
233
166
  const start = async () => {
234
167
  if (!isInstalled()) {
235
168
  return { success: false, error: 'CLIProxyAPI not installed', pid: null };
@@ -283,10 +216,7 @@ const start = async () => {
283
216
  }
284
217
  };
285
218
 
286
- /**
287
- * Stop CLIProxyAPI
288
- * @returns {Promise<Object>} { success, error }
289
- */
219
+ /** Stop CLIProxyAPI */
290
220
  const stop = async () => {
291
221
  const status = await isRunning();
292
222
  if (!status.running) {
@@ -341,144 +271,76 @@ const stop = async () => {
341
271
  }
342
272
  };
343
273
 
344
- /**
345
- * Ensure CLIProxyAPI is installed and running
346
- * @param {Function} onProgress - Progress callback
347
- * @returns {Promise<Object>} { success, error }
348
- */
274
+ /** Ensure CLIProxyAPI is installed and running */
349
275
  const ensureRunning = async (onProgress = null) => {
350
- // Check if installed
351
276
  if (!isInstalled()) {
352
277
  if (onProgress) onProgress('Installing CLIProxyAPI...', 0);
353
278
  const installResult = await install(onProgress);
354
- if (!installResult.success) {
355
- return installResult;
356
- }
279
+ if (!installResult.success) return installResult;
357
280
  }
358
-
359
- // Check if running
360
281
  const status = await isRunning();
361
- if (status.running) {
362
- return { success: true, error: null };
363
- }
364
-
365
- // Start
282
+ if (status.running) return { success: true, error: null };
366
283
  if (onProgress) onProgress('Starting CLIProxyAPI...', 100);
367
284
  return start();
368
285
  };
369
286
 
370
- /**
371
- * Get OAuth login URL for a provider
372
- * @param {string} provider - Provider ID (anthropic, openai, google, etc.)
373
- * @returns {Promise<Object>} { success, url, childProcess, isHeadless, error }
374
- */
287
+ /** Get OAuth login URL for a provider */
375
288
  const getLoginUrl = async (provider) => {
376
- // CLIProxyAPI login flags per provider (from --help)
377
289
  const providerFlags = {
378
- anthropic: '-claude-login', // Claude Code
379
- openai: '-codex-login', // OpenAI Codex
380
- google: '-login', // Gemini CLI (Google Account)
381
- qwen: '-qwen-login', // Qwen Code
382
- antigravity: '-antigravity-login', // Antigravity
383
- iflow: '-iflow-login' // iFlow
290
+ anthropic: '-claude-login', openai: '-codex-login', google: '-login',
291
+ qwen: '-qwen-login', antigravity: '-antigravity-login', iflow: '-iflow-login'
384
292
  };
385
-
386
293
  const flag = providerFlags[provider];
387
- if (!flag) {
388
- return { success: false, url: null, childProcess: null, isHeadless: false, error: 'Provider not supported for OAuth' };
389
- }
294
+ if (!flag) return { success: false, url: null, childProcess: null, isHeadless: false, error: 'Provider not supported for OAuth' };
390
295
 
391
296
  const headless = isHeadless();
392
-
393
- // For headless/VPS, use -no-browser flag only (don't pass config to avoid port conflict)
394
297
  return new Promise((resolve) => {
395
- const args = [flag, '-no-browser'];
396
- const child = spawn(BINARY_PATH, args, {
397
- cwd: INSTALL_DIR
398
- });
399
-
400
- let output = '';
401
- let resolved = false;
298
+ const child = spawn(BINARY_PATH, [flag, '-no-browser'], { cwd: INSTALL_DIR });
299
+ let output = '', resolved = false;
402
300
 
403
301
  const checkForUrl = () => {
404
302
  if (resolved) return;
405
303
  const urlMatch = output.match(/https?:\/\/[^\s]+/);
406
304
  if (urlMatch) {
407
305
  resolved = true;
408
- // Return child process so caller can wait for auth completion
409
306
  resolve({ success: true, url: urlMatch[0], childProcess: child, isHeadless: headless, error: null });
410
307
  }
411
308
  };
412
309
 
413
- child.stdout.on('data', (data) => {
414
- output += data.toString();
415
- checkForUrl();
416
- });
417
-
418
- child.stderr.on('data', (data) => {
419
- output += data.toString();
420
- checkForUrl();
421
- });
422
-
423
- child.on('error', (err) => {
424
- if (!resolved) {
425
- resolved = true;
426
- resolve({ success: false, url: null, childProcess: null, isHeadless: headless, error: err.message });
427
- }
428
- });
429
-
430
- child.on('close', (code) => {
431
- if (!resolved) {
432
- resolved = true;
433
- resolve({ success: false, url: null, childProcess: null, isHeadless: headless, error: `Process exited with code ${code}` });
434
- }
435
- });
310
+ child.stdout.on('data', (data) => { output += data.toString(); checkForUrl(); });
311
+ child.stderr.on('data', (data) => { output += data.toString(); checkForUrl(); });
312
+ child.on('error', (err) => { if (!resolved) { resolved = true; resolve({ success: false, url: null, childProcess: null, isHeadless: headless, error: err.message }); }});
313
+ child.on('close', (code) => { if (!resolved) { resolved = true; resolve({ success: false, url: null, childProcess: null, isHeadless: headless, error: `Process exited with code ${code}` }); }});
436
314
  });
437
315
  };
438
316
 
439
- /**
440
- * Process OAuth callback URL manually (for VPS/headless)
441
- * The callback URL looks like: http://localhost:54545/callback?code=xxx&state=yyy
442
- * We need to forward this to the waiting CLIProxyAPI process
443
- * @param {string} callbackUrl - The callback URL from the browser
444
- * @returns {Promise<Object>} { success, error }
445
- */
446
- const processCallback = (callbackUrl) => {
317
+ /** Get callback port for a provider */
318
+ const getCallbackPort = (provider) => CALLBACK_PORTS[provider] || null;
319
+
320
+ /** Process OAuth callback URL manually (for VPS/headless) */
321
+ const processCallback = (callbackUrl, provider = 'anthropic') => {
447
322
  return new Promise((resolve) => {
448
323
  try {
449
- // Parse the callback URL
450
324
  const url = new URL(callbackUrl);
451
- const params = url.searchParams;
325
+ const urlPort = url.port || (url.protocol === 'https:' ? 443 : 80);
326
+ const urlPath = url.pathname + url.search;
327
+ const expectedPort = CALLBACK_PORTS[provider];
452
328
 
453
- // Extract query string to forward
454
- const queryString = url.search;
329
+ if (!expectedPort) { resolve({ success: true, error: null }); return; } // Qwen uses polling
455
330
 
456
- // Make request to local callback endpoint
457
- const callbackPath = `/callback${queryString}`;
458
-
459
- const req = http.get(`http://127.0.0.1:${CALLBACK_PORT}${callbackPath}`, (res) => {
331
+ const targetPort = parseInt(urlPort) || expectedPort;
332
+ const req = http.get(`http://127.0.0.1:${targetPort}${urlPath}`, (res) => {
460
333
  let data = '';
461
334
  res.on('data', chunk => data += chunk);
462
335
  res.on('end', () => {
463
- if (res.statusCode === 200 || res.statusCode === 302) {
464
- resolve({ success: true, error: null });
465
- } else {
466
- resolve({ success: false, error: `Callback returned ${res.statusCode}: ${data}` });
467
- }
336
+ resolve(res.statusCode === 200 || res.statusCode === 302
337
+ ? { success: true, error: null }
338
+ : { success: false, error: `Callback returned ${res.statusCode}: ${data}` });
468
339
  });
469
340
  });
470
-
471
- req.on('error', (err) => {
472
- resolve({ success: false, error: `Callback error: ${err.message}` });
473
- });
474
-
475
- req.setTimeout(10000, () => {
476
- req.destroy();
477
- resolve({ success: false, error: 'Callback timeout' });
478
- });
479
- } catch (err) {
480
- resolve({ success: false, error: `Invalid URL: ${err.message}` });
481
- }
341
+ req.on('error', (err) => resolve({ success: false, error: `Callback error: ${err.message}` }));
342
+ req.setTimeout(10000, () => { req.destroy(); resolve({ success: false, error: 'Callback timeout' }); });
343
+ } catch (err) { resolve({ success: false, error: `Invalid URL: ${err.message}` }); }
482
344
  });
483
345
  };
484
346
 
@@ -488,7 +350,8 @@ module.exports = {
488
350
  BINARY_PATH,
489
351
  AUTH_DIR,
490
352
  DEFAULT_PORT,
491
- CALLBACK_PORT,
353
+ CALLBACK_PORTS,
354
+ CALLBACK_PATHS,
492
355
  getDownloadUrl,
493
356
  isInstalled,
494
357
  isHeadless,
@@ -498,5 +361,6 @@ module.exports = {
498
361
  stop,
499
362
  ensureRunning,
500
363
  getLoginUrl,
364
+ getCallbackPort,
501
365
  processCallback
502
366
  };