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 +1 -1
- package/src/pages/ai-agents.js +63 -79
- package/src/services/cliproxy/index.js +6 -2
- package/src/services/cliproxy/manager.js +81 -217
package/package.json
CHANGED
package/src/pages/ai-agents.js
CHANGED
|
@@ -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 (
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
236
|
-
|
|
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
|
-
|
|
261
|
+
// Process the callback - send to the login process
|
|
262
|
+
const spinner = ora({ text: 'PROCESSING CALLBACK...', color: 'yellow' }).start();
|
|
243
263
|
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
//
|
|
37
|
-
if (process.env.SSH_CLIENT || process.env.SSH_TTY || process.env.SSH_CONNECTION)
|
|
38
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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',
|
|
379
|
-
|
|
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
|
|
396
|
-
|
|
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
|
-
|
|
415
|
-
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
|
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
|
-
|
|
454
|
-
const queryString = url.search;
|
|
329
|
+
if (!expectedPort) { resolve({ success: true, error: null }); return; } // Qwen uses polling
|
|
455
330
|
|
|
456
|
-
|
|
457
|
-
const
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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.
|
|
472
|
-
|
|
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
|
-
|
|
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
|
};
|