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.
- package/package.json +1 -1
- package/src/app.js +64 -42
- package/src/menus/connect.js +17 -14
- package/src/menus/dashboard.js +76 -58
- package/src/pages/accounts.js +49 -38
- package/src/pages/ai-agents-ui.js +388 -0
- package/src/pages/ai-agents.js +494 -0
- package/src/pages/ai-models.js +389 -0
- package/src/pages/algo/algo-executor.js +307 -0
- package/src/pages/algo/copy-executor.js +331 -0
- package/src/pages/algo/copy-trading.js +178 -546
- package/src/pages/algo/custom-strategy.js +313 -0
- package/src/pages/algo/index.js +75 -18
- package/src/pages/algo/one-account.js +57 -322
- package/src/pages/algo/ui.js +15 -15
- package/src/pages/orders.js +22 -19
- package/src/pages/positions.js +22 -19
- package/src/pages/stats/index.js +16 -15
- package/src/pages/user.js +11 -7
- package/src/services/ai-supervision/consensus.js +284 -0
- package/src/services/ai-supervision/context.js +275 -0
- package/src/services/ai-supervision/directive.js +167 -0
- package/src/services/ai-supervision/health.js +47 -35
- package/src/services/ai-supervision/index.js +359 -0
- package/src/services/ai-supervision/parser.js +278 -0
- package/src/services/ai-supervision/symbols.js +259 -0
- package/src/services/cliproxy/index.js +256 -0
- package/src/services/cliproxy/installer.js +111 -0
- package/src/services/cliproxy/manager.js +387 -0
- package/src/services/index.js +9 -1
- package/src/services/llmproxy/index.js +166 -0
- package/src/services/llmproxy/manager.js +411 -0
- package/src/services/rithmic/accounts.js +6 -8
- package/src/ui/box.js +5 -9
- package/src/ui/index.js +18 -5
- 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
|
+
};
|