hedgequantx 2.7.28 → 2.7.30
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/app.js +21 -7
- package/src/pages/ai-agents.js +99 -53
- package/src/services/cliproxy/installer.js +111 -0
- package/src/services/cliproxy/manager.js +110 -117
- package/src/ui/index.js +8 -3
package/package.json
CHANGED
package/src/app.js
CHANGED
|
@@ -42,7 +42,15 @@ const restoreTerminal = () => {
|
|
|
42
42
|
};
|
|
43
43
|
|
|
44
44
|
process.on('exit', restoreTerminal);
|
|
45
|
-
process.on('SIGINT', () => {
|
|
45
|
+
process.on('SIGINT', () => {
|
|
46
|
+
restoreTerminal();
|
|
47
|
+
// Draw bottom border before exit
|
|
48
|
+
const termWidth = process.stdout.columns || 100;
|
|
49
|
+
const boxWidth = termWidth < 60 ? Math.max(termWidth - 2, 40) : Math.max(getLogoWidth(), 98);
|
|
50
|
+
console.log(chalk.cyan('\n╚' + '═'.repeat(boxWidth - 2) + '╝'));
|
|
51
|
+
console.log(chalk.gray('GOODBYE!'));
|
|
52
|
+
process.exit(0);
|
|
53
|
+
});
|
|
46
54
|
process.on('SIGTERM', () => { restoreTerminal(); process.exit(0); });
|
|
47
55
|
|
|
48
56
|
process.on('uncaughtException', (err) => {
|
|
@@ -162,20 +170,26 @@ const run = async () => {
|
|
|
162
170
|
try {
|
|
163
171
|
log.info('Starting HQX CLI');
|
|
164
172
|
|
|
165
|
-
// First launch - show banner with loading
|
|
173
|
+
// First launch - show banner with loading
|
|
166
174
|
await banner();
|
|
167
175
|
const boxWidth = getLogoWidth();
|
|
168
176
|
const innerWidth = boxWidth - 2;
|
|
177
|
+
|
|
178
|
+
// Show loading inside the box
|
|
169
179
|
console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
|
|
180
|
+
const loadingText = ' LOADING DASHBOARD...';
|
|
181
|
+
const loadingPad = innerWidth - loadingText.length;
|
|
182
|
+
process.stdout.write(chalk.cyan('║') + chalk.yellow(loadingText) + ' '.repeat(loadingPad) + chalk.cyan('║'));
|
|
170
183
|
|
|
171
|
-
const spinner = ora({ text: ' Loading...', color: 'yellow' }).start();
|
|
172
184
|
const restored = await connections.restoreFromStorage();
|
|
173
185
|
|
|
174
186
|
if (restored) {
|
|
175
187
|
currentService = connections.getAll()[0].service;
|
|
176
188
|
await refreshStats();
|
|
177
189
|
}
|
|
178
|
-
|
|
190
|
+
|
|
191
|
+
// Clear loading line
|
|
192
|
+
process.stdout.write('\r' + ' '.repeat(boxWidth + 2) + '\r');
|
|
179
193
|
|
|
180
194
|
// Main loop
|
|
181
195
|
while (true) {
|
|
@@ -234,13 +248,13 @@ const run = async () => {
|
|
|
234
248
|
}
|
|
235
249
|
|
|
236
250
|
console.log(chalk.cyan('╠' + '─'.repeat(innerWidth) + '╣'));
|
|
237
|
-
console.log(chalk.cyan('║') + chalk.red(centerText('[X]
|
|
251
|
+
console.log(chalk.cyan('║') + chalk.red(centerText('[X] EXIT', innerWidth)) + chalk.cyan('║'));
|
|
238
252
|
console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
|
|
239
253
|
|
|
240
|
-
const input = await prompts.textInput(chalk.cyan('
|
|
254
|
+
const input = await prompts.textInput(chalk.cyan('SELECT (1-' + numbered.length + '/X): '));
|
|
241
255
|
|
|
242
256
|
if (!input || input.toLowerCase() === 'x') {
|
|
243
|
-
console.log(chalk.gray('
|
|
257
|
+
console.log(chalk.gray('GOODBYE!'));
|
|
244
258
|
process.exit(0);
|
|
245
259
|
}
|
|
246
260
|
|
package/src/pages/ai-agents.js
CHANGED
|
@@ -11,12 +11,18 @@ const path = require('path');
|
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const ora = require('ora');
|
|
13
13
|
|
|
14
|
-
const { getLogoWidth } = require('../ui');
|
|
14
|
+
const { getLogoWidth, displayBanner } = require('../ui');
|
|
15
15
|
const { prompts } = require('../utils');
|
|
16
16
|
const { fetchModelsFromApi } = require('./ai-models');
|
|
17
17
|
const { drawProvidersTable, drawModelsTable, drawProviderWindow } = require('./ai-agents-ui');
|
|
18
18
|
const cliproxy = require('../services/cliproxy');
|
|
19
19
|
|
|
20
|
+
/** Clear screen and show banner */
|
|
21
|
+
const clearWithBanner = () => {
|
|
22
|
+
console.clear();
|
|
23
|
+
displayBanner();
|
|
24
|
+
};
|
|
25
|
+
|
|
20
26
|
// Config file path
|
|
21
27
|
const CONFIG_DIR = path.join(os.homedir(), '.hqx');
|
|
22
28
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'ai-config.json');
|
|
@@ -57,10 +63,10 @@ const saveConfig = (config) => {
|
|
|
57
63
|
/** Select a model from a pre-fetched list */
|
|
58
64
|
const selectModelFromList = async (provider, models, boxWidth) => {
|
|
59
65
|
while (true) {
|
|
60
|
-
|
|
66
|
+
clearWithBanner();
|
|
61
67
|
drawModelsTable(provider, models, boxWidth);
|
|
62
68
|
|
|
63
|
-
const input = await prompts.textInput(chalk.cyan('
|
|
69
|
+
const input = await prompts.textInput(chalk.cyan('SELECT MODEL: '));
|
|
64
70
|
const choice = (input || '').toLowerCase().trim();
|
|
65
71
|
|
|
66
72
|
if (choice === 'b' || choice === '') return null;
|
|
@@ -68,7 +74,7 @@ const selectModelFromList = async (provider, models, boxWidth) => {
|
|
|
68
74
|
const num = parseInt(choice);
|
|
69
75
|
if (!isNaN(num) && num >= 1 && num <= models.length) return models[num - 1];
|
|
70
76
|
|
|
71
|
-
console.log(chalk.red('
|
|
77
|
+
console.log(chalk.red(' INVALID OPTION'));
|
|
72
78
|
await new Promise(r => setTimeout(r, 1000));
|
|
73
79
|
}
|
|
74
80
|
};
|
|
@@ -76,16 +82,16 @@ const selectModelFromList = async (provider, models, boxWidth) => {
|
|
|
76
82
|
/** Select a model for a provider (fetches from API) */
|
|
77
83
|
const selectModel = async (provider, apiKey) => {
|
|
78
84
|
const boxWidth = getLogoWidth();
|
|
79
|
-
const spinner = ora({ text: '
|
|
85
|
+
const spinner = ora({ text: 'FETCHING MODELS FROM API...', color: 'yellow' }).start();
|
|
80
86
|
const result = await fetchModelsFromApi(provider.id, apiKey);
|
|
81
87
|
|
|
82
88
|
if (!result.success || result.models.length === 0) {
|
|
83
|
-
spinner.fail(result.error || '
|
|
89
|
+
spinner.fail(result.error || 'NO MODELS AVAILABLE');
|
|
84
90
|
await prompts.waitForEnter();
|
|
85
91
|
return null;
|
|
86
92
|
}
|
|
87
93
|
|
|
88
|
-
spinner.succeed(`
|
|
94
|
+
spinner.succeed(`FOUND ${result.models.length} MODELS`);
|
|
89
95
|
return selectModelFromList(provider, result.models, boxWidth);
|
|
90
96
|
};
|
|
91
97
|
|
|
@@ -104,47 +110,47 @@ const handleCliProxyConnection = async (provider, config, boxWidth) => {
|
|
|
104
110
|
|
|
105
111
|
// Check if CLIProxyAPI is installed
|
|
106
112
|
if (!cliproxy.isInstalled()) {
|
|
107
|
-
console.log(chalk.yellow('
|
|
108
|
-
const spinner = ora({ text: '
|
|
113
|
+
console.log(chalk.yellow(' CLIPROXYAPI NOT INSTALLED. INSTALLING...'));
|
|
114
|
+
const spinner = ora({ text: 'DOWNLOADING CLIPROXYAPI...', color: 'yellow' }).start();
|
|
109
115
|
|
|
110
116
|
const installResult = await cliproxy.install((msg, percent) => {
|
|
111
|
-
spinner.text = `${msg} ${percent}%`;
|
|
117
|
+
spinner.text = `${msg.toUpperCase()} ${percent}%`;
|
|
112
118
|
});
|
|
113
119
|
|
|
114
120
|
if (!installResult.success) {
|
|
115
|
-
spinner.fail(`
|
|
121
|
+
spinner.fail(`INSTALLATION FAILED: ${installResult.error.toUpperCase()}`);
|
|
116
122
|
await prompts.waitForEnter();
|
|
117
123
|
return false;
|
|
118
124
|
}
|
|
119
|
-
spinner.succeed('
|
|
125
|
+
spinner.succeed('CLIPROXYAPI INSTALLED');
|
|
120
126
|
}
|
|
121
127
|
|
|
122
128
|
// Check if running, start if not
|
|
123
129
|
let status = await cliproxy.isRunning();
|
|
124
130
|
if (!status.running) {
|
|
125
|
-
const spinner = ora({ text: '
|
|
131
|
+
const spinner = ora({ text: 'STARTING CLIPROXYAPI...', color: 'yellow' }).start();
|
|
126
132
|
const startResult = await cliproxy.start();
|
|
127
133
|
|
|
128
134
|
if (!startResult.success) {
|
|
129
|
-
spinner.fail(`
|
|
135
|
+
spinner.fail(`FAILED TO START: ${startResult.error.toUpperCase()}`);
|
|
130
136
|
await prompts.waitForEnter();
|
|
131
137
|
return false;
|
|
132
138
|
}
|
|
133
|
-
spinner.succeed('
|
|
139
|
+
spinner.succeed('CLIPROXYAPI STARTED');
|
|
134
140
|
} else {
|
|
135
|
-
console.log(chalk.green(' ✓
|
|
141
|
+
console.log(chalk.green(' ✓ CLIPROXYAPI IS RUNNING'));
|
|
136
142
|
}
|
|
137
143
|
|
|
138
144
|
// Check if provider supports OAuth
|
|
139
145
|
const oauthProviders = ['anthropic', 'openai', 'google', 'qwen'];
|
|
140
146
|
if (!oauthProviders.includes(provider.id)) {
|
|
141
147
|
// Try to fetch models directly
|
|
142
|
-
console.log(chalk.gray(`
|
|
148
|
+
console.log(chalk.gray(` CHECKING AVAILABLE MODELS FOR ${provider.name.toUpperCase()}...`));
|
|
143
149
|
const modelsResult = await cliproxy.fetchProviderModels(provider.id);
|
|
144
150
|
|
|
145
151
|
if (!modelsResult.success || modelsResult.models.length === 0) {
|
|
146
|
-
console.log(chalk.red(`
|
|
147
|
-
console.log(chalk.gray('
|
|
152
|
+
console.log(chalk.red(` NO MODELS AVAILABLE FOR ${provider.name.toUpperCase()}`));
|
|
153
|
+
console.log(chalk.gray(' THIS PROVIDER MAY REQUIRE API KEY CONNECTION.'));
|
|
148
154
|
await prompts.waitForEnter();
|
|
149
155
|
return false;
|
|
150
156
|
}
|
|
@@ -159,28 +165,68 @@ const handleCliProxyConnection = async (provider, config, boxWidth) => {
|
|
|
159
165
|
});
|
|
160
166
|
|
|
161
167
|
if (saveConfig(config)) {
|
|
162
|
-
console.log(chalk.green(`\n ✓ ${provider.name}
|
|
163
|
-
console.log(chalk.cyan(`
|
|
168
|
+
console.log(chalk.green(`\n ✓ ${provider.name.toUpperCase()} CONNECTED VIA CLIPROXY`));
|
|
169
|
+
console.log(chalk.cyan(` MODEL: ${selectedModel.name.toUpperCase()}`));
|
|
164
170
|
}
|
|
165
171
|
await prompts.waitForEnter();
|
|
166
172
|
return true;
|
|
167
173
|
}
|
|
168
174
|
|
|
169
175
|
// OAuth flow - get login URL
|
|
170
|
-
console.log(chalk.cyan(`\n
|
|
176
|
+
console.log(chalk.cyan(`\n STARTING OAUTH LOGIN FOR ${provider.name.toUpperCase()}...`));
|
|
171
177
|
const loginResult = await cliproxy.getLoginUrl(provider.id);
|
|
172
178
|
|
|
173
179
|
if (!loginResult.success) {
|
|
174
|
-
console.log(chalk.red(`
|
|
180
|
+
console.log(chalk.red(` OAUTH ERROR: ${loginResult.error.toUpperCase()}`));
|
|
175
181
|
await prompts.waitForEnter();
|
|
176
182
|
return false;
|
|
177
183
|
}
|
|
178
184
|
|
|
179
|
-
console.log(chalk.cyan('\n
|
|
185
|
+
console.log(chalk.cyan('\n OPEN THIS URL IN YOUR BROWSER TO AUTHENTICATE:\n'));
|
|
180
186
|
console.log(chalk.yellow(` ${loginResult.url}\n`));
|
|
181
|
-
console.log(chalk.gray(' After authenticating, press Enter to continue...'));
|
|
182
187
|
|
|
183
|
-
|
|
188
|
+
// Different flow for VPS/headless vs local
|
|
189
|
+
if (loginResult.isHeadless) {
|
|
190
|
+
console.log(chalk.magenta(' ══════════════════════════════════════════════════════'));
|
|
191
|
+
console.log(chalk.magenta(' VPS/SSH DETECTED - MANUAL CALLBACK REQUIRED'));
|
|
192
|
+
console.log(chalk.magenta(' ══════════════════════════════════════════════════════\n'));
|
|
193
|
+
console.log(chalk.white(' AFTER AUTHORIZING IN BROWSER, YOU WILL SEE A BLANK PAGE.'));
|
|
194
|
+
console.log(chalk.white(' COPY THE URL FROM BROWSER ADDRESS BAR (STARTS WITH LOCALHOST)'));
|
|
195
|
+
console.log(chalk.white(' AND PASTE IT BELOW:\n'));
|
|
196
|
+
|
|
197
|
+
const callbackUrl = await prompts.textInput(chalk.cyan(' CALLBACK URL: '));
|
|
198
|
+
|
|
199
|
+
if (!callbackUrl || !callbackUrl.includes('localhost')) {
|
|
200
|
+
console.log(chalk.red('\n INVALID CALLBACK URL'));
|
|
201
|
+
if (loginResult.childProcess) loginResult.childProcess.kill();
|
|
202
|
+
await prompts.waitForEnter();
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Process the callback
|
|
207
|
+
const spinner = ora({ text: 'PROCESSING CALLBACK...', color: 'yellow' }).start();
|
|
208
|
+
const callbackResult = await cliproxy.processCallback(callbackUrl.trim());
|
|
209
|
+
|
|
210
|
+
if (!callbackResult.success) {
|
|
211
|
+
spinner.fail(`CALLBACK FAILED: ${callbackResult.error.toUpperCase()}`);
|
|
212
|
+
if (loginResult.childProcess) loginResult.childProcess.kill();
|
|
213
|
+
await prompts.waitForEnter();
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
spinner.succeed('AUTHENTICATION SUCCESSFUL!');
|
|
218
|
+
|
|
219
|
+
// Wait for CLIProxyAPI to process the token
|
|
220
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
221
|
+
} else {
|
|
222
|
+
console.log(chalk.gray(' AFTER AUTHENTICATING, PRESS ENTER TO CONTINUE...'));
|
|
223
|
+
await prompts.waitForEnter();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Kill the login child process if still running
|
|
227
|
+
if (loginResult.childProcess) {
|
|
228
|
+
try { loginResult.childProcess.kill(); } catch (e) { /* ignore */ }
|
|
229
|
+
}
|
|
184
230
|
|
|
185
231
|
// Try to fetch models after auth
|
|
186
232
|
const modelsResult = await cliproxy.fetchProviderModels(provider.id);
|
|
@@ -194,8 +240,8 @@ const handleCliProxyConnection = async (provider, config, boxWidth) => {
|
|
|
194
240
|
modelName: selectedModel.name
|
|
195
241
|
});
|
|
196
242
|
if (saveConfig(config)) {
|
|
197
|
-
console.log(chalk.green(`\n ✓ ${provider.name}
|
|
198
|
-
console.log(chalk.cyan(`
|
|
243
|
+
console.log(chalk.green(`\n ✓ ${provider.name.toUpperCase()} CONNECTED VIA PAID PLAN`));
|
|
244
|
+
console.log(chalk.cyan(` MODEL: ${selectedModel.name.toUpperCase()}`));
|
|
199
245
|
}
|
|
200
246
|
}
|
|
201
247
|
} else {
|
|
@@ -203,10 +249,10 @@ const handleCliProxyConnection = async (provider, config, boxWidth) => {
|
|
|
203
249
|
activateProvider(config, provider.id, {
|
|
204
250
|
connectionType: 'cliproxy',
|
|
205
251
|
modelId: null,
|
|
206
|
-
modelName: '
|
|
252
|
+
modelName: 'AUTO'
|
|
207
253
|
});
|
|
208
254
|
if (saveConfig(config)) {
|
|
209
|
-
console.log(chalk.green(`\n ✓ ${provider.name}
|
|
255
|
+
console.log(chalk.green(`\n ✓ ${provider.name.toUpperCase()} CONNECTED VIA PAID PLAN`));
|
|
210
256
|
}
|
|
211
257
|
}
|
|
212
258
|
|
|
@@ -216,20 +262,20 @@ const handleCliProxyConnection = async (provider, config, boxWidth) => {
|
|
|
216
262
|
|
|
217
263
|
/** Handle API Key connection */
|
|
218
264
|
const handleApiKeyConnection = async (provider, config) => {
|
|
219
|
-
|
|
220
|
-
console.log(chalk.yellow(`\n
|
|
221
|
-
console.log(chalk.gray(' (
|
|
265
|
+
clearWithBanner();
|
|
266
|
+
console.log(chalk.yellow(`\n ENTER YOUR ${provider.name.toUpperCase()} API KEY:`));
|
|
267
|
+
console.log(chalk.gray(' (PRESS ENTER TO CANCEL)\n'));
|
|
222
268
|
|
|
223
|
-
const apiKey = await prompts.textInput(chalk.cyan(' API
|
|
269
|
+
const apiKey = await prompts.textInput(chalk.cyan(' API KEY: '), true);
|
|
224
270
|
|
|
225
271
|
if (!apiKey || apiKey.trim() === '') {
|
|
226
|
-
console.log(chalk.gray('
|
|
272
|
+
console.log(chalk.gray(' CANCELLED'));
|
|
227
273
|
await prompts.waitForEnter();
|
|
228
274
|
return false;
|
|
229
275
|
}
|
|
230
276
|
|
|
231
277
|
if (apiKey.length < 20) {
|
|
232
|
-
console.log(chalk.red('
|
|
278
|
+
console.log(chalk.red(' INVALID API KEY FORMAT (TOO SHORT)'));
|
|
233
279
|
await prompts.waitForEnter();
|
|
234
280
|
return false;
|
|
235
281
|
}
|
|
@@ -245,10 +291,10 @@ const handleApiKeyConnection = async (provider, config) => {
|
|
|
245
291
|
});
|
|
246
292
|
|
|
247
293
|
if (saveConfig(config)) {
|
|
248
|
-
console.log(chalk.green(`\n ✓ ${provider.name}
|
|
249
|
-
console.log(chalk.cyan(`
|
|
294
|
+
console.log(chalk.green(`\n ✓ ${provider.name.toUpperCase()} CONNECTED VIA API KEY`));
|
|
295
|
+
console.log(chalk.cyan(` MODEL: ${selectedModel.name.toUpperCase()}`));
|
|
250
296
|
} else {
|
|
251
|
-
console.log(chalk.red('\n
|
|
297
|
+
console.log(chalk.red('\n FAILED TO SAVE CONFIG'));
|
|
252
298
|
}
|
|
253
299
|
await prompts.waitForEnter();
|
|
254
300
|
return true;
|
|
@@ -259,10 +305,10 @@ const handleProviderConfig = async (provider, config) => {
|
|
|
259
305
|
const boxWidth = getLogoWidth();
|
|
260
306
|
|
|
261
307
|
while (true) {
|
|
262
|
-
|
|
308
|
+
clearWithBanner();
|
|
263
309
|
drawProviderWindow(provider, config, boxWidth);
|
|
264
310
|
|
|
265
|
-
const input = await prompts.textInput(chalk.cyan('
|
|
311
|
+
const input = await prompts.textInput(chalk.cyan('SELECT OPTION: '));
|
|
266
312
|
const choice = (input || '').toLowerCase().trim();
|
|
267
313
|
|
|
268
314
|
if (choice === 'b' || choice === '') break;
|
|
@@ -270,7 +316,7 @@ const handleProviderConfig = async (provider, config) => {
|
|
|
270
316
|
if (choice === 'd' && config.providers[provider.id]) {
|
|
271
317
|
config.providers[provider.id].active = false;
|
|
272
318
|
saveConfig(config);
|
|
273
|
-
console.log(chalk.yellow(`\n ${provider.name}
|
|
319
|
+
console.log(chalk.yellow(`\n ${provider.name.toUpperCase()} DISCONNECTED`));
|
|
274
320
|
await prompts.waitForEnter();
|
|
275
321
|
continue;
|
|
276
322
|
}
|
|
@@ -313,18 +359,18 @@ const getActiveAgentCount = () => getActiveProvider() ? 1 : 0;
|
|
|
313
359
|
|
|
314
360
|
/** Show CLIProxy status */
|
|
315
361
|
const showCliProxyStatus = async () => {
|
|
316
|
-
|
|
317
|
-
console.log(chalk.yellow('\n
|
|
362
|
+
clearWithBanner();
|
|
363
|
+
console.log(chalk.yellow('\n CLIPROXYAPI STATUS\n'));
|
|
318
364
|
|
|
319
365
|
const installed = cliproxy.isInstalled();
|
|
320
|
-
console.log(chalk.gray('
|
|
366
|
+
console.log(chalk.gray(' INSTALLED: ') + (installed ? chalk.green('YES') : chalk.red('NO')));
|
|
321
367
|
|
|
322
368
|
if (installed) {
|
|
323
369
|
const status = await cliproxy.isRunning();
|
|
324
|
-
console.log(chalk.gray('
|
|
325
|
-
console.log(chalk.gray('
|
|
326
|
-
console.log(chalk.gray('
|
|
327
|
-
console.log(chalk.gray('
|
|
370
|
+
console.log(chalk.gray(' RUNNING: ') + (status.running ? chalk.green('YES') : chalk.red('NO')));
|
|
371
|
+
console.log(chalk.gray(' VERSION: ') + chalk.cyan(cliproxy.CLIPROXY_VERSION));
|
|
372
|
+
console.log(chalk.gray(' PORT: ') + chalk.cyan(cliproxy.DEFAULT_PORT));
|
|
373
|
+
console.log(chalk.gray(' INSTALL DIR: ') + chalk.cyan(cliproxy.INSTALL_DIR));
|
|
328
374
|
}
|
|
329
375
|
|
|
330
376
|
console.log();
|
|
@@ -337,12 +383,12 @@ const aiAgentsMenu = async () => {
|
|
|
337
383
|
const boxWidth = getLogoWidth();
|
|
338
384
|
|
|
339
385
|
while (true) {
|
|
340
|
-
|
|
386
|
+
clearWithBanner();
|
|
341
387
|
const status = await cliproxy.isRunning();
|
|
342
|
-
const statusText = status.running ? `
|
|
388
|
+
const statusText = status.running ? `LOCALHOST:${cliproxy.DEFAULT_PORT}` : 'NOT RUNNING';
|
|
343
389
|
drawProvidersTable(AI_PROVIDERS, config, boxWidth, statusText);
|
|
344
390
|
|
|
345
|
-
const input = await prompts.textInput(chalk.cyan('
|
|
391
|
+
const input = await prompts.textInput(chalk.cyan('SELECT (1-8/S/B): '));
|
|
346
392
|
const choice = (input || '').toLowerCase().trim();
|
|
347
393
|
|
|
348
394
|
if (choice === 'b' || choice === '') break;
|
|
@@ -358,7 +404,7 @@ const aiAgentsMenu = async () => {
|
|
|
358
404
|
continue;
|
|
359
405
|
}
|
|
360
406
|
|
|
361
|
-
console.log(chalk.red('
|
|
407
|
+
console.log(chalk.red(' INVALID OPTION'));
|
|
362
408
|
await new Promise(r => setTimeout(r, 1000));
|
|
363
409
|
}
|
|
364
410
|
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLIProxyAPI Installer
|
|
3
|
+
*
|
|
4
|
+
* Handles downloading and extracting CLIProxyAPI binary
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const https = require('https');
|
|
9
|
+
const http = require('http');
|
|
10
|
+
const { createGunzip } = require('zlib');
|
|
11
|
+
const tar = require('tar');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Download file from URL with redirect support
|
|
15
|
+
* @param {string} url - URL to download
|
|
16
|
+
* @param {string} destPath - Destination path
|
|
17
|
+
* @param {Function} onProgress - Progress callback (percent)
|
|
18
|
+
* @returns {Promise<boolean>}
|
|
19
|
+
*/
|
|
20
|
+
const downloadFile = (url, destPath, onProgress = null) => {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
const file = fs.createWriteStream(destPath);
|
|
23
|
+
|
|
24
|
+
const request = (url.startsWith('https') ? https : http).get(url, (response) => {
|
|
25
|
+
// Handle redirects
|
|
26
|
+
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
27
|
+
file.close();
|
|
28
|
+
fs.unlinkSync(destPath);
|
|
29
|
+
return downloadFile(response.headers.location, destPath, onProgress)
|
|
30
|
+
.then(resolve)
|
|
31
|
+
.catch(reject);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (response.statusCode !== 200) {
|
|
35
|
+
file.close();
|
|
36
|
+
fs.unlinkSync(destPath);
|
|
37
|
+
return reject(new Error(`HTTP ${response.statusCode}`));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const totalSize = parseInt(response.headers['content-length'], 10);
|
|
41
|
+
let downloadedSize = 0;
|
|
42
|
+
|
|
43
|
+
response.on('data', (chunk) => {
|
|
44
|
+
downloadedSize += chunk.length;
|
|
45
|
+
if (onProgress && totalSize) {
|
|
46
|
+
onProgress(Math.round((downloadedSize / totalSize) * 100));
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
response.pipe(file);
|
|
51
|
+
|
|
52
|
+
file.on('finish', () => {
|
|
53
|
+
file.close();
|
|
54
|
+
resolve(true);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
request.on('error', (err) => {
|
|
59
|
+
file.close();
|
|
60
|
+
if (fs.existsSync(destPath)) fs.unlinkSync(destPath);
|
|
61
|
+
reject(err);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
request.setTimeout(120000, () => {
|
|
65
|
+
request.destroy();
|
|
66
|
+
reject(new Error('Download timeout'));
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Extract tar.gz file
|
|
73
|
+
* @param {string} archivePath - Path to archive
|
|
74
|
+
* @param {string} destDir - Destination directory
|
|
75
|
+
* @returns {Promise<boolean>}
|
|
76
|
+
*/
|
|
77
|
+
const extractTarGz = (archivePath, destDir) => {
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
fs.createReadStream(archivePath)
|
|
80
|
+
.pipe(createGunzip())
|
|
81
|
+
.pipe(tar.extract({ cwd: destDir }))
|
|
82
|
+
.on('finish', () => resolve(true))
|
|
83
|
+
.on('error', reject);
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Extract zip file (Windows)
|
|
89
|
+
* @param {string} archivePath - Path to archive
|
|
90
|
+
* @param {string} destDir - Destination directory
|
|
91
|
+
* @returns {Promise<boolean>}
|
|
92
|
+
*/
|
|
93
|
+
const extractZip = async (archivePath, destDir) => {
|
|
94
|
+
const { execSync } = require('child_process');
|
|
95
|
+
|
|
96
|
+
if (process.platform === 'win32') {
|
|
97
|
+
execSync(`powershell -Command "Expand-Archive -Path '${archivePath}' -DestinationPath '${destDir}' -Force"`, {
|
|
98
|
+
stdio: 'ignore'
|
|
99
|
+
});
|
|
100
|
+
} else {
|
|
101
|
+
execSync(`unzip -o "${archivePath}" -d "${destDir}"`, { stdio: 'ignore' });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return true;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
module.exports = {
|
|
108
|
+
downloadFile,
|
|
109
|
+
extractTarGz,
|
|
110
|
+
extractZip
|
|
111
|
+
};
|
|
@@ -8,11 +8,9 @@
|
|
|
8
8
|
const os = require('os');
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const fs = require('fs');
|
|
11
|
-
const https = require('https');
|
|
12
11
|
const http = require('http');
|
|
13
12
|
const { spawn } = require('child_process');
|
|
14
|
-
const {
|
|
15
|
-
const tar = require('tar');
|
|
13
|
+
const { downloadFile, extractTarGz, extractZip } = require('./installer');
|
|
16
14
|
|
|
17
15
|
// CLIProxyAPI version and download URLs
|
|
18
16
|
const CLIPROXY_VERSION = '6.6.88';
|
|
@@ -27,6 +25,29 @@ const AUTH_DIR = path.join(INSTALL_DIR, 'auths');
|
|
|
27
25
|
|
|
28
26
|
// Default port
|
|
29
27
|
const DEFAULT_PORT = 8317;
|
|
28
|
+
const CALLBACK_PORT = 54545;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Detect if running in headless/VPS environment (no display)
|
|
32
|
+
* @returns {boolean}
|
|
33
|
+
*/
|
|
34
|
+
const isHeadless = () => {
|
|
35
|
+
// Check for common display environment variables
|
|
36
|
+
if (process.env.DISPLAY) return false;
|
|
37
|
+
if (process.env.WAYLAND_DISPLAY) return false;
|
|
38
|
+
|
|
39
|
+
// Check if running via SSH
|
|
40
|
+
if (process.env.SSH_CLIENT || process.env.SSH_TTY || process.env.SSH_CONNECTION) return true;
|
|
41
|
+
|
|
42
|
+
// Check platform-specific indicators
|
|
43
|
+
if (process.platform === 'linux') {
|
|
44
|
+
// No DISPLAY usually means headless on Linux
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// macOS/Windows usually have a display
|
|
49
|
+
return false;
|
|
50
|
+
};
|
|
30
51
|
|
|
31
52
|
/**
|
|
32
53
|
* Get download URL for current platform
|
|
@@ -73,101 +94,6 @@ const isInstalled = () => {
|
|
|
73
94
|
return fs.existsSync(BINARY_PATH);
|
|
74
95
|
};
|
|
75
96
|
|
|
76
|
-
/**
|
|
77
|
-
* Download file from URL
|
|
78
|
-
* @param {string} url - URL to download
|
|
79
|
-
* @param {string} destPath - Destination path
|
|
80
|
-
* @param {Function} onProgress - Progress callback (percent)
|
|
81
|
-
* @returns {Promise<boolean>}
|
|
82
|
-
*/
|
|
83
|
-
const downloadFile = (url, destPath, onProgress = null) => {
|
|
84
|
-
return new Promise((resolve, reject) => {
|
|
85
|
-
const file = fs.createWriteStream(destPath);
|
|
86
|
-
|
|
87
|
-
const request = (url.startsWith('https') ? https : http).get(url, (response) => {
|
|
88
|
-
// Handle redirects
|
|
89
|
-
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
90
|
-
file.close();
|
|
91
|
-
fs.unlinkSync(destPath);
|
|
92
|
-
return downloadFile(response.headers.location, destPath, onProgress)
|
|
93
|
-
.then(resolve)
|
|
94
|
-
.catch(reject);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (response.statusCode !== 200) {
|
|
98
|
-
file.close();
|
|
99
|
-
fs.unlinkSync(destPath);
|
|
100
|
-
return reject(new Error(`HTTP ${response.statusCode}`));
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const totalSize = parseInt(response.headers['content-length'], 10);
|
|
104
|
-
let downloadedSize = 0;
|
|
105
|
-
|
|
106
|
-
response.on('data', (chunk) => {
|
|
107
|
-
downloadedSize += chunk.length;
|
|
108
|
-
if (onProgress && totalSize) {
|
|
109
|
-
onProgress(Math.round((downloadedSize / totalSize) * 100));
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
response.pipe(file);
|
|
114
|
-
|
|
115
|
-
file.on('finish', () => {
|
|
116
|
-
file.close();
|
|
117
|
-
resolve(true);
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
request.on('error', (err) => {
|
|
122
|
-
file.close();
|
|
123
|
-
if (fs.existsSync(destPath)) fs.unlinkSync(destPath);
|
|
124
|
-
reject(err);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
request.setTimeout(120000, () => {
|
|
128
|
-
request.destroy();
|
|
129
|
-
reject(new Error('Download timeout'));
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Extract tar.gz file
|
|
136
|
-
* @param {string} archivePath - Path to archive
|
|
137
|
-
* @param {string} destDir - Destination directory
|
|
138
|
-
* @returns {Promise<boolean>}
|
|
139
|
-
*/
|
|
140
|
-
const extractTarGz = (archivePath, destDir) => {
|
|
141
|
-
return new Promise((resolve, reject) => {
|
|
142
|
-
fs.createReadStream(archivePath)
|
|
143
|
-
.pipe(createGunzip())
|
|
144
|
-
.pipe(tar.extract({ cwd: destDir }))
|
|
145
|
-
.on('finish', () => resolve(true))
|
|
146
|
-
.on('error', reject);
|
|
147
|
-
});
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Extract zip file (Windows)
|
|
152
|
-
* @param {string} archivePath - Path to archive
|
|
153
|
-
* @param {string} destDir - Destination directory
|
|
154
|
-
* @returns {Promise<boolean>}
|
|
155
|
-
*/
|
|
156
|
-
const extractZip = async (archivePath, destDir) => {
|
|
157
|
-
const { execSync } = require('child_process');
|
|
158
|
-
|
|
159
|
-
if (process.platform === 'win32') {
|
|
160
|
-
// Use PowerShell on Windows
|
|
161
|
-
execSync(`powershell -Command "Expand-Archive -Path '${archivePath}' -DestinationPath '${destDir}' -Force"`, {
|
|
162
|
-
stdio: 'ignore'
|
|
163
|
-
});
|
|
164
|
-
} else {
|
|
165
|
-
// Use unzip on Unix
|
|
166
|
-
execSync(`unzip -o "${archivePath}" -d "${destDir}"`, { stdio: 'ignore' });
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return true;
|
|
170
|
-
};
|
|
171
97
|
|
|
172
98
|
/**
|
|
173
99
|
* Install CLIProxyAPI
|
|
@@ -388,48 +314,112 @@ const ensureRunning = async (onProgress = null) => {
|
|
|
388
314
|
/**
|
|
389
315
|
* Get OAuth login URL for a provider
|
|
390
316
|
* @param {string} provider - Provider ID (anthropic, openai, google, etc.)
|
|
391
|
-
* @returns {Promise<Object>} { success, url, error }
|
|
317
|
+
* @returns {Promise<Object>} { success, url, childProcess, isHeadless, error }
|
|
392
318
|
*/
|
|
393
319
|
const getLoginUrl = async (provider) => {
|
|
394
320
|
const providerFlags = {
|
|
395
|
-
anthropic: '
|
|
396
|
-
openai: '
|
|
397
|
-
google: '
|
|
398
|
-
qwen: '
|
|
321
|
+
anthropic: '-claude-login',
|
|
322
|
+
openai: '-codex-login',
|
|
323
|
+
google: '-gemini-login',
|
|
324
|
+
qwen: '-qwen-login'
|
|
399
325
|
};
|
|
400
326
|
|
|
401
327
|
const flag = providerFlags[provider];
|
|
402
328
|
if (!flag) {
|
|
403
|
-
return { success: false, url: null, error: 'Provider not supported for OAuth' };
|
|
329
|
+
return { success: false, url: null, childProcess: null, isHeadless: false, error: 'Provider not supported for OAuth' };
|
|
404
330
|
}
|
|
405
331
|
|
|
406
|
-
|
|
332
|
+
const headless = isHeadless();
|
|
333
|
+
|
|
334
|
+
// For headless/VPS, use -no-browser flag
|
|
407
335
|
return new Promise((resolve) => {
|
|
408
|
-
const args = [flag, '
|
|
336
|
+
const args = [flag, '-no-browser'];
|
|
409
337
|
const child = spawn(BINARY_PATH, args, {
|
|
410
|
-
cwd: INSTALL_DIR
|
|
411
|
-
env: { ...process.env, AUTH_DIR: AUTH_DIR }
|
|
338
|
+
cwd: INSTALL_DIR
|
|
412
339
|
});
|
|
413
340
|
|
|
414
341
|
let output = '';
|
|
342
|
+
let resolved = false;
|
|
343
|
+
|
|
344
|
+
const checkForUrl = () => {
|
|
345
|
+
if (resolved) return;
|
|
346
|
+
const urlMatch = output.match(/https?:\/\/[^\s]+/);
|
|
347
|
+
if (urlMatch) {
|
|
348
|
+
resolved = true;
|
|
349
|
+
// Return child process so caller can wait for auth completion
|
|
350
|
+
resolve({ success: true, url: urlMatch[0], childProcess: child, isHeadless: headless, error: null });
|
|
351
|
+
}
|
|
352
|
+
};
|
|
415
353
|
|
|
416
354
|
child.stdout.on('data', (data) => {
|
|
417
355
|
output += data.toString();
|
|
356
|
+
checkForUrl();
|
|
418
357
|
});
|
|
419
358
|
|
|
420
359
|
child.stderr.on('data', (data) => {
|
|
421
360
|
output += data.toString();
|
|
361
|
+
checkForUrl();
|
|
422
362
|
});
|
|
423
363
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
resolve({ success: true, url: urlMatch[0], error: null });
|
|
429
|
-
} else {
|
|
430
|
-
resolve({ success: false, url: null, error: 'Could not get login URL' });
|
|
364
|
+
child.on('error', (err) => {
|
|
365
|
+
if (!resolved) {
|
|
366
|
+
resolved = true;
|
|
367
|
+
resolve({ success: false, url: null, childProcess: null, isHeadless: headless, error: err.message });
|
|
431
368
|
}
|
|
432
|
-
}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
child.on('close', (code) => {
|
|
372
|
+
if (!resolved) {
|
|
373
|
+
resolved = true;
|
|
374
|
+
resolve({ success: false, url: null, childProcess: null, isHeadless: headless, error: `Process exited with code ${code}` });
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Process OAuth callback URL manually (for VPS/headless)
|
|
382
|
+
* The callback URL looks like: http://localhost:54545/callback?code=xxx&state=yyy
|
|
383
|
+
* We need to forward this to the waiting CLIProxyAPI process
|
|
384
|
+
* @param {string} callbackUrl - The callback URL from the browser
|
|
385
|
+
* @returns {Promise<Object>} { success, error }
|
|
386
|
+
*/
|
|
387
|
+
const processCallback = (callbackUrl) => {
|
|
388
|
+
return new Promise((resolve) => {
|
|
389
|
+
try {
|
|
390
|
+
// Parse the callback URL
|
|
391
|
+
const url = new URL(callbackUrl);
|
|
392
|
+
const params = url.searchParams;
|
|
393
|
+
|
|
394
|
+
// Extract query string to forward
|
|
395
|
+
const queryString = url.search;
|
|
396
|
+
|
|
397
|
+
// Make request to local callback endpoint
|
|
398
|
+
const callbackPath = `/callback${queryString}`;
|
|
399
|
+
|
|
400
|
+
const req = http.get(`http://127.0.0.1:${CALLBACK_PORT}${callbackPath}`, (res) => {
|
|
401
|
+
let data = '';
|
|
402
|
+
res.on('data', chunk => data += chunk);
|
|
403
|
+
res.on('end', () => {
|
|
404
|
+
if (res.statusCode === 200 || res.statusCode === 302) {
|
|
405
|
+
resolve({ success: true, error: null });
|
|
406
|
+
} else {
|
|
407
|
+
resolve({ success: false, error: `Callback returned ${res.statusCode}: ${data}` });
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
req.on('error', (err) => {
|
|
413
|
+
resolve({ success: false, error: `Callback error: ${err.message}` });
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
req.setTimeout(10000, () => {
|
|
417
|
+
req.destroy();
|
|
418
|
+
resolve({ success: false, error: 'Callback timeout' });
|
|
419
|
+
});
|
|
420
|
+
} catch (err) {
|
|
421
|
+
resolve({ success: false, error: `Invalid URL: ${err.message}` });
|
|
422
|
+
}
|
|
433
423
|
});
|
|
434
424
|
};
|
|
435
425
|
|
|
@@ -439,12 +429,15 @@ module.exports = {
|
|
|
439
429
|
BINARY_PATH,
|
|
440
430
|
AUTH_DIR,
|
|
441
431
|
DEFAULT_PORT,
|
|
432
|
+
CALLBACK_PORT,
|
|
442
433
|
getDownloadUrl,
|
|
443
434
|
isInstalled,
|
|
435
|
+
isHeadless,
|
|
444
436
|
install,
|
|
445
437
|
isRunning,
|
|
446
438
|
start,
|
|
447
439
|
stop,
|
|
448
440
|
ensureRunning,
|
|
449
|
-
getLoginUrl
|
|
441
|
+
getLoginUrl,
|
|
442
|
+
processCallback
|
|
450
443
|
};
|
package/src/ui/index.js
CHANGED
|
@@ -26,10 +26,10 @@ const {
|
|
|
26
26
|
const { createBoxMenu } = require('./menu');
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
* Display HQX Banner (without closing border)
|
|
30
|
-
*
|
|
29
|
+
* Display HQX Banner (without closing border by default)
|
|
30
|
+
* @param {boolean} closed - If true, add bottom border
|
|
31
31
|
*/
|
|
32
|
-
const displayBanner = () => {
|
|
32
|
+
const displayBanner = (closed = false) => {
|
|
33
33
|
const termWidth = process.stdout.columns || 100;
|
|
34
34
|
const isMobile = termWidth < 60;
|
|
35
35
|
const boxWidth = isMobile ? Math.max(termWidth - 2, 40) : Math.max(getLogoWidth(), 98);
|
|
@@ -72,6 +72,11 @@ const displayBanner = () => {
|
|
|
72
72
|
console.log(chalk.cyan('╠' + '═'.repeat(innerWidth) + '╣'));
|
|
73
73
|
const tagline = isMobile ? `HQX v${version}` : `Prop Futures Algo Trading v${version}`;
|
|
74
74
|
console.log(chalk.cyan('║') + chalk.white(centerText(tagline, innerWidth)) + chalk.cyan('║'));
|
|
75
|
+
|
|
76
|
+
// Close the box if requested
|
|
77
|
+
if (closed) {
|
|
78
|
+
console.log(chalk.cyan('╚' + '═'.repeat(innerWidth) + '╝'));
|
|
79
|
+
}
|
|
75
80
|
};
|
|
76
81
|
|
|
77
82
|
/**
|