hedgequantx 2.9.20 → 2.9.22

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 (36) hide show
  1. package/package.json +1 -1
  2. package/src/app.js +64 -42
  3. package/src/menus/connect.js +17 -14
  4. package/src/menus/dashboard.js +76 -58
  5. package/src/pages/accounts.js +49 -38
  6. package/src/pages/ai-agents-ui.js +388 -0
  7. package/src/pages/ai-agents.js +494 -0
  8. package/src/pages/ai-models.js +389 -0
  9. package/src/pages/algo/algo-executor.js +307 -0
  10. package/src/pages/algo/copy-executor.js +331 -0
  11. package/src/pages/algo/copy-trading.js +178 -546
  12. package/src/pages/algo/custom-strategy.js +313 -0
  13. package/src/pages/algo/index.js +75 -18
  14. package/src/pages/algo/one-account.js +57 -322
  15. package/src/pages/algo/ui.js +15 -15
  16. package/src/pages/orders.js +22 -19
  17. package/src/pages/positions.js +22 -19
  18. package/src/pages/stats/index.js +16 -15
  19. package/src/pages/user.js +11 -7
  20. package/src/services/ai-supervision/consensus.js +284 -0
  21. package/src/services/ai-supervision/context.js +275 -0
  22. package/src/services/ai-supervision/directive.js +167 -0
  23. package/src/services/ai-supervision/health.js +47 -35
  24. package/src/services/ai-supervision/index.js +359 -0
  25. package/src/services/ai-supervision/parser.js +278 -0
  26. package/src/services/ai-supervision/symbols.js +259 -0
  27. package/src/services/cliproxy/index.js +256 -0
  28. package/src/services/cliproxy/installer.js +111 -0
  29. package/src/services/cliproxy/manager.js +387 -0
  30. package/src/services/index.js +9 -1
  31. package/src/services/llmproxy/index.js +166 -0
  32. package/src/services/llmproxy/manager.js +411 -0
  33. package/src/services/rithmic/accounts.js +6 -8
  34. package/src/ui/box.js +5 -9
  35. package/src/ui/index.js +18 -5
  36. package/src/ui/menu.js +4 -4
@@ -0,0 +1,494 @@
1
+ /** AI Agents Configuration Page - HQX Connector (OAuth) + API Key */
2
+
3
+ const chalk = require('chalk');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const fs = require('fs');
7
+ const ora = require('ora');
8
+
9
+ const { getLogoWidth, displayBanner, clearScreen } = require('../ui');
10
+ const { prompts } = require('../utils');
11
+ const { fetchModelsFromApi } = require('./ai-models');
12
+ const { drawProvidersTable, drawModelsTable, drawProviderWindow, drawConnectionTest } = require('./ai-agents-ui');
13
+ const cliproxy = require('../services/cliproxy');
14
+
15
+ /** Clear screen and show banner (always closed) */
16
+ const clearWithBanner = () => {
17
+ clearScreen();
18
+ displayBanner(); // Banner always closed
19
+ };
20
+
21
+ // Config file path
22
+ const CONFIG_DIR = path.join(os.homedir(), '.hqx');
23
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'ai-config.json');
24
+
25
+ // AI Providers list with OAuth (paid plan) and API Key support
26
+ // HQX Connector (port 8317): OAuth for Anthropic, OpenAI, Google, Qwen, iFlow
27
+ // Direct API Key: For MiniMax, DeepSeek, Mistral, xAI, OpenRouter
28
+ const AI_PROVIDERS = [
29
+ // OAuth + API Key supported (can use paid plan OR API key)
30
+ { id: 'anthropic', name: 'Anthropic (Claude)', color: 'magenta', supportsOAuth: true, supportsApiKey: true },
31
+ { id: 'openai', name: 'OpenAI (GPT)', color: 'green', supportsOAuth: true, supportsApiKey: true },
32
+ { id: 'google', name: 'Google (Gemini)', color: 'blue', supportsOAuth: true, supportsApiKey: true },
33
+ { id: 'qwen', name: 'Qwen', color: 'cyan', supportsOAuth: true, supportsApiKey: true },
34
+ { id: 'iflow', name: 'iFlow (DeepSeek/GLM)', color: 'yellow', supportsOAuth: true, supportsApiKey: true },
35
+ // API Key only (no OAuth - uses LLM Proxy via LiteLLM)
36
+ { id: 'minimax', name: 'MiniMax', color: 'magenta', supportsOAuth: false, supportsApiKey: true },
37
+ { id: 'deepseek', name: 'DeepSeek', color: 'blue', supportsOAuth: false, supportsApiKey: true },
38
+ { id: 'mistral', name: 'Mistral AI', color: 'yellow', supportsOAuth: false, supportsApiKey: true },
39
+ { id: 'xai', name: 'xAI (Grok)', color: 'white', supportsOAuth: false, supportsApiKey: true },
40
+ { id: 'openrouter', name: 'OpenRouter', color: 'gray', supportsOAuth: false, supportsApiKey: true },
41
+ ];
42
+
43
+ /** Load AI config from file */
44
+ const loadConfig = () => {
45
+ try {
46
+ if (fs.existsSync(CONFIG_FILE)) {
47
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
48
+ }
49
+ } catch (error) { /* ignore */ }
50
+ return { providers: {} };
51
+ };
52
+
53
+ /** Save AI config to file */
54
+ const saveConfig = (config) => {
55
+ try {
56
+ if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
57
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
58
+ return true;
59
+ } catch (error) {
60
+ return false;
61
+ }
62
+ };
63
+
64
+ /** Select a model from a pre-fetched list */
65
+ const selectModelFromList = async (provider, models, boxWidth) => {
66
+ while (true) {
67
+ clearWithBanner();
68
+ drawModelsTable(provider, models, boxWidth);
69
+
70
+ const input = await prompts.textInput(chalk.cyan('SELECT MODEL: '));
71
+ const choice = (input || '').toLowerCase().trim();
72
+
73
+ if (choice === 'b' || choice === '') return null;
74
+
75
+ const num = parseInt(choice);
76
+ if (!isNaN(num) && num >= 1 && num <= models.length) return models[num - 1];
77
+
78
+ console.log(chalk.red(' INVALID OPTION'));
79
+ await new Promise(r => setTimeout(r, 1000));
80
+ }
81
+ };
82
+
83
+ /** Select a model for a provider (fetches from API) */
84
+ const selectModel = async (provider, apiKey) => {
85
+ const boxWidth = getLogoWidth();
86
+ const spinner = ora({ text: 'FETCHING MODELS FROM API...', color: 'yellow' }).start();
87
+ const result = await fetchModelsFromApi(provider.id, apiKey);
88
+
89
+ if (!result.success || result.models.length === 0) {
90
+ spinner.fail(result.error || 'NO MODELS AVAILABLE');
91
+ await prompts.waitForEnter();
92
+ return null;
93
+ }
94
+
95
+ spinner.succeed(`FOUND ${result.models.length} MODELS`);
96
+ return selectModelFromList(provider, result.models, boxWidth);
97
+ };
98
+
99
+ /** Activate a provider (multiple providers can be active at the same time) */
100
+ const activateProvider = (config, providerId, data) => {
101
+ if (!config.providers[providerId]) config.providers[providerId] = {};
102
+ Object.assign(config.providers[providerId], data, { active: true, configuredAt: new Date().toISOString() });
103
+ };
104
+
105
+ /** Wait for child process to exit */
106
+ const waitForProcessExit = (cp, timeoutMs = 15000, intervalMs = 500) => new Promise((resolve) => {
107
+ if (!cp) return resolve();
108
+ let elapsed = 0;
109
+ const check = setInterval(() => {
110
+ elapsed += intervalMs;
111
+ if (cp.exitCode !== null || cp.killed || elapsed >= timeoutMs) {
112
+ clearInterval(check);
113
+ if (elapsed >= timeoutMs) try { cp.kill(); } catch (e) {}
114
+ resolve();
115
+ }
116
+ }, intervalMs);
117
+ });
118
+
119
+ /** Handle HQX Connector connection (with auto-install) */
120
+ const handleCliProxyConnection = async (provider, config, boxWidth) => {
121
+ console.log();
122
+ // Check/install HQX Connector
123
+ if (!cliproxy.isInstalled()) {
124
+ console.log(chalk.yellow(' HQX CONNECTOR NOT INSTALLED. INSTALLING...'));
125
+ const spinner = ora({ text: 'DOWNLOADING...', color: 'yellow' }).start();
126
+ const installResult = await cliproxy.install((msg, percent) => { spinner.text = `${msg.toUpperCase()} ${percent}%`; });
127
+ if (!installResult.success) { spinner.fail(`INSTALL FAILED: ${installResult.error}`); await prompts.waitForEnter(); return false; }
128
+ spinner.succeed('HQX CONNECTOR INSTALLED');
129
+ }
130
+ // Check/start HQX Connector
131
+ let status = await cliproxy.isRunning();
132
+ if (!status.running) {
133
+ const spinner = ora({ text: 'STARTING HQX CONNECTOR...', color: 'yellow' }).start();
134
+ const startResult = await cliproxy.start();
135
+ if (!startResult.success) { spinner.fail(`START FAILED: ${startResult.error}`); await prompts.waitForEnter(); return false; }
136
+ spinner.succeed('HQX CONNECTOR STARTED');
137
+ } else {
138
+ const cfgPath = path.join(os.homedir(), '.hqx', 'cliproxy', 'config.yaml');
139
+ if (!fs.existsSync(cfgPath)) {
140
+ console.log(chalk.yellow(' RESTARTING HQX CONNECTOR...'));
141
+ await cliproxy.stop();
142
+ const res = await cliproxy.start();
143
+ if (!res.success) { console.log(chalk.red(` RESTART FAILED: ${res.error}`)); await prompts.waitForEnter(); return false; }
144
+ console.log(chalk.green(' ✓ RESTARTED'));
145
+ } else console.log(chalk.green(' ✓ HQX CONNECTOR RUNNING'));
146
+ }
147
+
148
+ // First, check if models are already available (existing auth)
149
+ console.log(chalk.gray(` CHECKING EXISTING AUTHENTICATION...`));
150
+
151
+ const existingModels = await cliproxy.fetchProviderModels(provider.id);
152
+
153
+
154
+ if (existingModels.success && existingModels.models.length > 0) {
155
+ // Models already available - skip OAuth, go directly to model selection
156
+ console.log(chalk.green(` ✓ ALREADY AUTHENTICATED`));
157
+ const selectedModel = await selectModelFromList(provider, existingModels.models, boxWidth);
158
+ if (!selectedModel) return false;
159
+
160
+ activateProvider(config, provider.id, {
161
+ connectionType: 'cliproxy',
162
+ modelId: selectedModel.id,
163
+ modelName: selectedModel.name
164
+ });
165
+
166
+ if (saveConfig(config)) {
167
+ console.log(chalk.green(`\n ✓ ${provider.name.toUpperCase()} CONNECTED VIA CLIPROXY`));
168
+ console.log(chalk.cyan(` MODEL: ${selectedModel.name.toUpperCase()}`));
169
+ }
170
+ await prompts.waitForEnter();
171
+ return true;
172
+ }
173
+
174
+ // Check if provider supports OAuth
175
+ const oauthProviders = ['anthropic', 'openai', 'google', 'qwen'];
176
+ if (!oauthProviders.includes(provider.id)) {
177
+ console.log(chalk.red(` NO MODELS AVAILABLE FOR ${provider.name.toUpperCase()}`));
178
+ console.log(chalk.gray(' THIS PROVIDER MAY REQUIRE API KEY CONNECTION.'));
179
+ await prompts.waitForEnter();
180
+ return false;
181
+ }
182
+
183
+ // OAuth flow - get login URL
184
+ console.log(chalk.cyan(`\n STARTING OAUTH LOGIN FOR ${provider.name.toUpperCase()}...`));
185
+ const loginResult = await cliproxy.getLoginUrl(provider.id);
186
+
187
+ if (!loginResult.success) {
188
+ console.log(chalk.red(` OAUTH ERROR: ${loginResult.error.toUpperCase()}`));
189
+ await prompts.waitForEnter();
190
+ return false;
191
+ }
192
+
193
+ console.log(chalk.cyan('\n OPEN THIS URL IN YOUR BROWSER TO AUTHENTICATE:\n'));
194
+ console.log(chalk.yellow(` ${loginResult.url}\n`));
195
+
196
+ // Get callback port for this provider
197
+ const callbackPort = cliproxy.getCallbackPort(provider.id);
198
+ const isPollingAuth = (provider.id === 'qwen'); // Qwen uses polling, not callback
199
+
200
+ // Different flow for VPS/headless vs local
201
+ if (loginResult.isHeadless) {
202
+ console.log(chalk.magenta(' ══════════════════════════════════════════════════════════'));
203
+ console.log(chalk.magenta(' VPS/SSH DETECTED - MANUAL CALLBACK REQUIRED'));
204
+ console.log(chalk.magenta(' ══════════════════════════════════════════════════════════\n'));
205
+
206
+ if (isPollingAuth) {
207
+ // Qwen uses polling - just wait for user to authorize
208
+ console.log(chalk.white(' 1. OPEN THE URL ABOVE IN YOUR BROWSER'));
209
+ console.log(chalk.white(' 2. AUTHORIZE THE APPLICATION'));
210
+ console.log(chalk.white(' 3. WAIT FOR AUTHENTICATION TO COMPLETE'));
211
+ console.log(chalk.white(' 4. PRESS ENTER WHEN DONE\n'));
212
+ await prompts.waitForEnter();
213
+
214
+ const spinner = ora({ text: 'WAITING FOR AUTHENTICATION...', color: 'yellow' }).start();
215
+ await waitForProcessExit(loginResult.childProcess, 90000, 1000);
216
+ spinner.succeed('AUTHENTICATION COMPLETED!');
217
+ } else {
218
+ // Standard OAuth with callback
219
+ console.log(chalk.white(' 1. OPEN THE URL ABOVE IN YOUR LOCAL BROWSER'));
220
+ console.log(chalk.white(' 2. AUTHORIZE THE APPLICATION'));
221
+ console.log(chalk.white(' 3. YOU WILL SEE A BLANK PAGE - THIS IS NORMAL'));
222
+ console.log(chalk.white(' 4. COPY THE FULL URL FROM YOUR BROWSER ADDRESS BAR'));
223
+ console.log(chalk.white(` (IT STARTS WITH: http://localhost:${callbackPort}/...)`));
224
+ console.log(chalk.white(' 5. PASTE IT BELOW:\n'));
225
+
226
+ const callbackUrl = await prompts.textInput(chalk.cyan(' CALLBACK URL: '));
227
+
228
+ if (!callbackUrl || !callbackUrl.includes('localhost')) {
229
+ console.log(chalk.red('\n INVALID CALLBACK URL'));
230
+ if (loginResult.childProcess) loginResult.childProcess.kill();
231
+ await prompts.waitForEnter();
232
+ return false;
233
+ }
234
+
235
+ // Process the callback - send to the login process
236
+ const spinner = ora({ text: 'PROCESSING CALLBACK...', color: 'yellow' }).start();
237
+
238
+ try {
239
+ const callbackResult = await cliproxy.processCallback(callbackUrl.trim(), provider.id);
240
+
241
+ if (!callbackResult.success) {
242
+ spinner.fail(`CALLBACK FAILED: ${callbackResult.error}`);
243
+ if (loginResult.childProcess) loginResult.childProcess.kill();
244
+ await prompts.waitForEnter();
245
+ return false;
246
+ }
247
+
248
+ spinner.text = 'EXCHANGING TOKEN...';
249
+ await waitForProcessExit(loginResult.childProcess);
250
+ spinner.succeed('AUTHENTICATION SUCCESSFUL!');
251
+ } catch (err) {
252
+ spinner.fail(`ERROR: ${err.message}`);
253
+ if (loginResult.childProcess) loginResult.childProcess.kill();
254
+ await prompts.waitForEnter();
255
+ return false;
256
+ }
257
+ }
258
+
259
+ } else {
260
+ // Local machine - browser opens automatically, wait for user to auth
261
+ console.log(chalk.gray(' AFTER AUTHENTICATING IN YOUR BROWSER, PRESS ENTER...'));
262
+ await prompts.waitForEnter();
263
+
264
+ const spinner = ora({ text: 'SAVING AUTHENTICATION...', color: 'yellow' }).start();
265
+ await waitForProcessExit(loginResult.childProcess);
266
+ spinner.succeed('AUTHENTICATION SAVED');
267
+ }
268
+
269
+ // Small delay for CLIProxy to detect new auth file
270
+ const spinner = ora({ text: 'LOADING MODELS...', color: 'yellow' }).start();
271
+ await new Promise(r => setTimeout(r, 2000));
272
+
273
+ // Fetch models from CLIProxy API
274
+ const modelsResult = await cliproxy.fetchProviderModels(provider.id);
275
+ spinner.stop();
276
+
277
+ if (modelsResult.success && modelsResult.models.length > 0) {
278
+ const selectedModel = await selectModelFromList(provider, modelsResult.models, boxWidth);
279
+ if (selectedModel) {
280
+ activateProvider(config, provider.id, {
281
+ connectionType: 'cliproxy',
282
+ modelId: selectedModel.id,
283
+ modelName: selectedModel.name
284
+ });
285
+ saveConfig(config);
286
+ console.log(chalk.green(`\n ✓ ${provider.name.toUpperCase()} CONNECTED`));
287
+ console.log(chalk.cyan(` MODEL: ${selectedModel.name.toUpperCase()}`));
288
+ await prompts.waitForEnter();
289
+ return true;
290
+ }
291
+ // User pressed B to go back - still save as connected but no model selected yet
292
+ return true;
293
+ }
294
+
295
+ // No models found - show error
296
+ console.log(chalk.red('\n NO MODELS AVAILABLE'));
297
+ console.log(chalk.gray(' TRY RECONNECTING OR USE API KEY'));
298
+ await prompts.waitForEnter();
299
+ return false;
300
+ };
301
+
302
+ /** Handle API Key connection */
303
+ const handleApiKeyConnection = async (provider, config) => {
304
+ clearWithBanner();
305
+ console.log(chalk.yellow(`\n ENTER YOUR ${provider.name.toUpperCase()} API KEY:`));
306
+ console.log(chalk.gray(' (PRESS ENTER TO CANCEL)\n'));
307
+
308
+ const apiKey = await prompts.textInput(chalk.cyan(' API KEY: '), true);
309
+
310
+ if (!apiKey || apiKey.trim() === '') {
311
+ console.log(chalk.gray(' CANCELLED'));
312
+ await prompts.waitForEnter();
313
+ return false;
314
+ }
315
+
316
+ if (apiKey.length < 20) {
317
+ console.log(chalk.red(' INVALID API KEY FORMAT (TOO SHORT)'));
318
+ await prompts.waitForEnter();
319
+ return false;
320
+ }
321
+
322
+ const selectedModel = await selectModel(provider, apiKey.trim());
323
+ if (!selectedModel) return false;
324
+
325
+ activateProvider(config, provider.id, {
326
+ connectionType: 'apikey',
327
+ apiKey: apiKey.trim(),
328
+ modelId: selectedModel.id,
329
+ modelName: selectedModel.name
330
+ });
331
+
332
+ if (saveConfig(config)) {
333
+ console.log(chalk.green(`\n ✓ ${provider.name.toUpperCase()} CONNECTED VIA API KEY`));
334
+ console.log(chalk.cyan(` MODEL: ${selectedModel.name.toUpperCase()}`));
335
+ } else {
336
+ console.log(chalk.red('\n FAILED TO SAVE CONFIG'));
337
+ }
338
+ await prompts.waitForEnter();
339
+ return true;
340
+ };
341
+
342
+ /** Handle provider configuration */
343
+ const handleProviderConfig = async (provider, config) => {
344
+ const boxWidth = getLogoWidth();
345
+
346
+ // Check provider capabilities
347
+ const supportsOAuth = provider.supportsOAuth !== false;
348
+ const supportsApiKey = provider.supportsApiKey !== false;
349
+
350
+ while (true) {
351
+ clearWithBanner();
352
+ drawProviderWindow(provider, config, boxWidth);
353
+
354
+ const input = await prompts.textInput(chalk.cyan('SELECT OPTION: '));
355
+ const choice = (input || '').toLowerCase().trim();
356
+
357
+ if (choice === 'b' || choice === '') break;
358
+
359
+ if (choice === 'd' && config.providers[provider.id]) {
360
+ config.providers[provider.id].active = false;
361
+ saveConfig(config);
362
+ console.log(chalk.yellow(`\n ${provider.name.toUpperCase()} DISCONNECTED`));
363
+ await prompts.waitForEnter();
364
+ continue;
365
+ }
366
+
367
+ if (choice === '1') {
368
+ if (supportsOAuth && supportsApiKey) {
369
+ // Both supported: [1] = OAuth via CLIProxy
370
+ await handleCliProxyConnection(provider, config, boxWidth);
371
+ } else if (supportsApiKey) {
372
+ // API Key only: [1] = API Key via LLM Proxy
373
+ await handleApiKeyConnection(provider, config);
374
+ } else if (supportsOAuth) {
375
+ // OAuth only: [1] = OAuth via CLIProxy
376
+ await handleCliProxyConnection(provider, config, boxWidth);
377
+ }
378
+ continue;
379
+ }
380
+
381
+ if (choice === '2' && supportsOAuth && supportsApiKey) {
382
+ // Only available when both are supported: [2] = API Key
383
+ await handleApiKeyConnection(provider, config);
384
+ continue;
385
+ }
386
+ }
387
+
388
+ return config;
389
+ };
390
+
391
+ /** Get active AI provider config (legacy - single provider) */
392
+ const getActiveProvider = () => {
393
+ const config = loadConfig();
394
+ for (const provider of AI_PROVIDERS) {
395
+ const pc = config.providers[provider.id];
396
+ if (pc && pc.active) {
397
+ return {
398
+ id: provider.id,
399
+ name: provider.name,
400
+ connectionType: pc.connectionType,
401
+ apiKey: pc.apiKey || null,
402
+ modelId: pc.modelId || null,
403
+ modelName: pc.modelName || null,
404
+ weight: pc.weight || 100
405
+ };
406
+ }
407
+ }
408
+ return null;
409
+ };
410
+
411
+ /** Get ALL active AI agents for multi-agent supervision */
412
+ const getActiveAgents = () => {
413
+ const config = loadConfig();
414
+ const agents = [];
415
+
416
+ for (const provider of AI_PROVIDERS) {
417
+ const pc = config.providers[provider.id];
418
+ if (pc && pc.active) {
419
+ agents.push({
420
+ id: `agent-${provider.id}`,
421
+ provider: provider.id,
422
+ name: provider.name,
423
+ connectionType: pc.connectionType,
424
+ apiKey: pc.apiKey || null,
425
+ modelId: pc.modelId || null,
426
+ modelName: pc.modelName || null,
427
+ weight: pc.weight || Math.floor(100 / AI_PROVIDERS.filter(p => config.providers[p.id]?.active).length),
428
+ active: true
429
+ });
430
+ }
431
+ }
432
+
433
+ return agents;
434
+ };
435
+
436
+ /** Get supervision config for SupervisionEngine */
437
+ const getSupervisionConfig = () => {
438
+ const agents = getActiveAgents();
439
+ return {
440
+ supervisionEnabled: agents.length > 0,
441
+ agents,
442
+ minAgents: 1,
443
+ timeout: 30000
444
+ };
445
+ };
446
+
447
+ /** Count active AI agents */
448
+ const getActiveAgentCount = () => getActiveAgents().length;
449
+
450
+ /** Main AI Agents menu */
451
+ const aiAgentsMenu = async () => {
452
+ let config = loadConfig();
453
+ const boxWidth = getLogoWidth();
454
+
455
+ while (true) {
456
+ clearWithBanner();
457
+ const agentCount = getActiveAgentCount();
458
+ drawProvidersTable(AI_PROVIDERS, config, boxWidth, agentCount > 0);
459
+ console.log();
460
+
461
+ const promptText = agentCount > 0 ? 'SELECT (1-8/T/B): ' : 'SELECT (1-8/B): ';
462
+ const input = await prompts.textInput(chalk.cyan(promptText));
463
+ const choice = (input || '').toLowerCase().trim();
464
+
465
+ if (choice === 'b' || choice === '') break;
466
+
467
+ if (choice === 't' && agentCount > 0) {
468
+ const agents = getActiveAgents();
469
+ await drawConnectionTest(agents, boxWidth, clearWithBanner);
470
+ await prompts.waitForEnter();
471
+ continue;
472
+ }
473
+
474
+ const num = parseInt(choice);
475
+ if (!isNaN(num) && num >= 1 && num <= AI_PROVIDERS.length) {
476
+ config = await handleProviderConfig(AI_PROVIDERS[num - 1], config);
477
+ continue;
478
+ }
479
+
480
+ console.log(chalk.red(' INVALID OPTION'));
481
+ await new Promise(r => setTimeout(r, 1000));
482
+ }
483
+ };
484
+
485
+ module.exports = {
486
+ aiAgentsMenu,
487
+ getActiveProvider,
488
+ getActiveAgents,
489
+ getSupervisionConfig,
490
+ getActiveAgentCount,
491
+ loadConfig,
492
+ saveConfig,
493
+ AI_PROVIDERS
494
+ };