hedgequantx 2.7.18 → 2.7.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "2.7.18",
3
+ "version": "2.7.19",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
package/src/app.js CHANGED
@@ -17,6 +17,7 @@ const log = logger.scope('App');
17
17
  const { showStats } = require('./pages/stats');
18
18
  const { showAccounts } = require('./pages/accounts');
19
19
  const { algoTradingMenu } = require('./pages/algo');
20
+ const { aiAgentsMenu, getActiveAgentCount } = require('./pages/ai-agents');
20
21
 
21
22
  // Menus
22
23
  const { rithmicMenu, dashboardMenu, handleUpdate } = require('./menus');
@@ -296,9 +297,7 @@ const run = async () => {
296
297
  break;
297
298
 
298
299
  case 'aiagents':
299
- console.log(chalk.yellow('\n AI Agents - Coming soon!'));
300
- console.log(chalk.gray(' Configure AI trading agents for automated strategies.'));
301
- await prompts.waitForEnter();
300
+ await aiAgentsMenu();
302
301
  break;
303
302
 
304
303
  case 'update':
@@ -0,0 +1,407 @@
1
+ /**
2
+ * AI Agents Configuration Page
3
+ *
4
+ * Allows users to configure AI providers for trading strategies.
5
+ * Supports both CLIProxy (paid plans) and direct API keys.
6
+ */
7
+
8
+ const chalk = require('chalk');
9
+ const os = require('os');
10
+ const path = require('path');
11
+ const fs = require('fs');
12
+
13
+ const { getLogoWidth, centerText, visibleLength } = require('../ui');
14
+ const { prompts } = require('../utils');
15
+
16
+ // Config file path
17
+ const CONFIG_DIR = path.join(os.homedir(), '.hqx');
18
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'ai-config.json');
19
+
20
+ // AI Providers list
21
+ const AI_PROVIDERS = [
22
+ { id: 'anthropic', name: 'Anthropic (Claude)', color: 'magenta' },
23
+ { id: 'openai', name: 'OpenAI (GPT)', color: 'green' },
24
+ { id: 'google', name: 'Google (Gemini)', color: 'blue' },
25
+ { id: 'mistral', name: 'Mistral AI', color: 'yellow' },
26
+ { id: 'groq', name: 'Groq', color: 'cyan' },
27
+ { id: 'xai', name: 'xAI (Grok)', color: 'white' },
28
+ { id: 'perplexity', name: 'Perplexity', color: 'blue' },
29
+ { id: 'openrouter', name: 'OpenRouter', color: 'gray' },
30
+ ];
31
+
32
+ /**
33
+ * Load AI config from file
34
+ * @returns {Object} Config object with provider settings
35
+ */
36
+ const loadConfig = () => {
37
+ try {
38
+ if (fs.existsSync(CONFIG_FILE)) {
39
+ const data = fs.readFileSync(CONFIG_FILE, 'utf8');
40
+ return JSON.parse(data);
41
+ }
42
+ } catch (error) {
43
+ // Config file doesn't exist or is invalid
44
+ }
45
+ return { providers: {} };
46
+ };
47
+
48
+ /**
49
+ * Save AI config to file
50
+ * @param {Object} config - Config object to save
51
+ * @returns {boolean} Success status
52
+ */
53
+ const saveConfig = (config) => {
54
+ try {
55
+ if (!fs.existsSync(CONFIG_DIR)) {
56
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
57
+ }
58
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
59
+ return true;
60
+ } catch (error) {
61
+ return false;
62
+ }
63
+ };
64
+
65
+ /**
66
+ * Mask API key for display (show first 8 and last 4 chars)
67
+ * @param {string} key - API key
68
+ * @returns {string} Masked key
69
+ */
70
+ const maskKey = (key) => {
71
+ if (!key || key.length < 16) return '****';
72
+ return key.substring(0, 8) + '...' + key.substring(key.length - 4);
73
+ };
74
+
75
+ /**
76
+ * Draw the main providers selection table (2 columns)
77
+ * @param {Object} config - Current config
78
+ * @param {number} boxWidth - Box width
79
+ */
80
+ const drawProvidersTable = (config, boxWidth) => {
81
+ const W = boxWidth - 2;
82
+ const col1Width = Math.floor(W / 2);
83
+ const col2Width = W - col1Width;
84
+
85
+ // Header
86
+ console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
87
+ console.log(chalk.cyan('║') + chalk.yellow.bold(centerText('AI AGENTS CONFIGURATION', W)) + chalk.cyan('║'));
88
+ console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
89
+
90
+ // Calculate max name length for alignment
91
+ const maxNameLen = Math.max(...AI_PROVIDERS.map(p => p.name.length));
92
+
93
+ // Provider rows (2 columns)
94
+ const rows = Math.ceil(AI_PROVIDERS.length / 2);
95
+ for (let row = 0; row < rows; row++) {
96
+ const leftIdx = row;
97
+ const rightIdx = row + rows;
98
+
99
+ const leftProvider = AI_PROVIDERS[leftIdx];
100
+ const rightProvider = AI_PROVIDERS[rightIdx];
101
+
102
+ // Left column
103
+ const leftNum = `[${leftIdx + 1}]`;
104
+ const leftName = leftProvider.name;
105
+ const leftConfig = config.providers[leftProvider.id] || {};
106
+ const leftStatus = leftConfig.active ? chalk.green('●') : '';
107
+ const leftText = chalk.cyan(leftNum) + ' ' + chalk[leftProvider.color](leftName) + ' ' + leftStatus;
108
+ const leftLen = visibleLength(leftText);
109
+ const leftPadTotal = col1Width - leftLen;
110
+ const leftPadL = Math.floor(leftPadTotal / 2);
111
+ const leftPadR = leftPadTotal - leftPadL;
112
+
113
+ // Right column
114
+ let rightText = '';
115
+ let rightPadL = 0;
116
+ let rightPadR = col2Width;
117
+ if (rightProvider) {
118
+ const rightNum = `[${rightIdx + 1}]`;
119
+ const rightName = rightProvider.name;
120
+ const rightConfig = config.providers[rightProvider.id] || {};
121
+ const rightStatus = rightConfig.active ? chalk.green('●') : '';
122
+ rightText = chalk.cyan(rightNum) + ' ' + chalk[rightProvider.color](rightName) + ' ' + rightStatus;
123
+ const rightLen = visibleLength(rightText);
124
+ const rightPadTotal = col2Width - rightLen;
125
+ rightPadL = Math.floor(rightPadTotal / 2);
126
+ rightPadR = rightPadTotal - rightPadL;
127
+ }
128
+
129
+ console.log(
130
+ chalk.cyan('║') +
131
+ ' '.repeat(leftPadL) + leftText + ' '.repeat(leftPadR) +
132
+ ' '.repeat(rightPadL) + rightText + ' '.repeat(rightPadR) +
133
+ chalk.cyan('║')
134
+ );
135
+ }
136
+
137
+ // Footer
138
+ console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
139
+ console.log(chalk.cyan('║') + chalk.red(centerText('[B] Back to Menu', W)) + chalk.cyan('║'));
140
+ console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
141
+ };
142
+
143
+ /**
144
+ * Draw provider configuration window
145
+ * @param {Object} provider - Provider object
146
+ * @param {Object} config - Current config
147
+ * @param {number} boxWidth - Box width
148
+ */
149
+ const drawProviderWindow = (provider, config, boxWidth) => {
150
+ const W = boxWidth - 2;
151
+ const col1Width = Math.floor(W / 2);
152
+ const col2Width = W - col1Width;
153
+ const providerConfig = config.providers[provider.id] || {};
154
+
155
+ // Header
156
+ console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
157
+ console.log(chalk.cyan('║') + chalk[provider.color].bold(centerText(provider.name.toUpperCase(), W)) + chalk.cyan('║'));
158
+ console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
159
+
160
+ // Empty line
161
+ console.log(chalk.cyan('║') + ' '.repeat(W) + chalk.cyan('║'));
162
+
163
+ // Options in 2 columns
164
+ const opt1Title = '[1] Connect via Paid Plan';
165
+ const opt1Desc = 'Uses CLIProxy - No API key needed';
166
+ const opt2Title = '[2] Connect via API Key';
167
+ const opt2Desc = 'Enter your own API key';
168
+
169
+ // Row 1: Titles
170
+ const left1 = chalk.green(opt1Title);
171
+ const right1 = chalk.yellow(opt2Title);
172
+ const left1Len = visibleLength(left1);
173
+ const right1Len = visibleLength(right1);
174
+ const left1PadTotal = col1Width - left1Len;
175
+ const left1PadL = Math.floor(left1PadTotal / 2);
176
+ const left1PadR = left1PadTotal - left1PadL;
177
+ const right1PadTotal = col2Width - right1Len;
178
+ const right1PadL = Math.floor(right1PadTotal / 2);
179
+ const right1PadR = right1PadTotal - right1PadL;
180
+
181
+ console.log(
182
+ chalk.cyan('║') +
183
+ ' '.repeat(left1PadL) + left1 + ' '.repeat(left1PadR) +
184
+ ' '.repeat(right1PadL) + right1 + ' '.repeat(right1PadR) +
185
+ chalk.cyan('║')
186
+ );
187
+
188
+ // Row 2: Descriptions
189
+ const left2 = chalk.gray(opt1Desc);
190
+ const right2 = chalk.gray(opt2Desc);
191
+ const left2Len = visibleLength(left2);
192
+ const right2Len = visibleLength(right2);
193
+ const left2PadTotal = col1Width - left2Len;
194
+ const left2PadL = Math.floor(left2PadTotal / 2);
195
+ const left2PadR = left2PadTotal - left2PadL;
196
+ const right2PadTotal = col2Width - right2Len;
197
+ const right2PadL = Math.floor(right2PadTotal / 2);
198
+ const right2PadR = right2PadTotal - right2PadL;
199
+
200
+ console.log(
201
+ chalk.cyan('║') +
202
+ ' '.repeat(left2PadL) + left2 + ' '.repeat(left2PadR) +
203
+ ' '.repeat(right2PadL) + right2 + ' '.repeat(right2PadR) +
204
+ chalk.cyan('║')
205
+ );
206
+
207
+ // Empty line
208
+ console.log(chalk.cyan('║') + ' '.repeat(W) + chalk.cyan('║'));
209
+
210
+ // Status bar
211
+ console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
212
+
213
+ let statusText = '';
214
+ if (providerConfig.active) {
215
+ const connType = providerConfig.connectionType === 'cliproxy' ? 'CLIProxy' : 'API Key';
216
+ const keyDisplay = providerConfig.apiKey ? maskKey(providerConfig.apiKey) : 'N/A';
217
+ statusText = chalk.green('● ACTIVE') + chalk.gray(' via ') + chalk.cyan(connType);
218
+ if (providerConfig.connectionType === 'apikey' && providerConfig.apiKey) {
219
+ statusText += chalk.gray(' Key: ') + chalk.cyan(keyDisplay);
220
+ }
221
+ } else if (providerConfig.apiKey || providerConfig.connectionType) {
222
+ statusText = chalk.yellow('● CONFIGURED') + chalk.gray(' (not active)');
223
+ } else {
224
+ statusText = chalk.gray('○ NOT CONFIGURED');
225
+ }
226
+ console.log(chalk.cyan('║') + centerText(statusText, W) + chalk.cyan('║'));
227
+
228
+ // Disconnect option if active
229
+ if (providerConfig.active) {
230
+ console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
231
+ console.log(chalk.cyan('║') + chalk.red(centerText('[D] Disconnect', W)) + chalk.cyan('║'));
232
+ }
233
+
234
+ // Back
235
+ console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
236
+ console.log(chalk.cyan('║') + chalk.red(centerText('[B] Back', W)) + chalk.cyan('║'));
237
+ console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
238
+ };
239
+
240
+ /**
241
+ * Handle provider configuration
242
+ * @param {Object} provider - Provider to configure
243
+ * @param {Object} config - Current config
244
+ * @returns {Object} Updated config
245
+ */
246
+ const handleProviderConfig = async (provider, config) => {
247
+ const boxWidth = getLogoWidth();
248
+
249
+ while (true) {
250
+ console.clear();
251
+ drawProviderWindow(provider, config, boxWidth);
252
+
253
+ const input = await prompts.textInput(chalk.cyan('Select option: '));
254
+ const choice = (input || '').toLowerCase().trim();
255
+
256
+ if (choice === 'b' || choice === '') {
257
+ break;
258
+ }
259
+
260
+ if (choice === 'd') {
261
+ // Disconnect
262
+ if (config.providers[provider.id]) {
263
+ config.providers[provider.id].active = false;
264
+ saveConfig(config);
265
+ console.log(chalk.yellow(`\n ${provider.name} disconnected.`));
266
+ await prompts.waitForEnter();
267
+ }
268
+ continue;
269
+ }
270
+
271
+ if (choice === '1') {
272
+ // CLIProxy connection
273
+ console.log();
274
+ console.log(chalk.cyan(' Connecting via CLIProxy...'));
275
+ console.log(chalk.gray(' This uses your paid plan (Claude Pro, ChatGPT Plus, etc.)'));
276
+ console.log();
277
+
278
+ // Deactivate all other providers
279
+ Object.keys(config.providers).forEach(id => {
280
+ if (config.providers[id]) config.providers[id].active = false;
281
+ });
282
+
283
+ if (!config.providers[provider.id]) config.providers[provider.id] = {};
284
+ config.providers[provider.id].connectionType = 'cliproxy';
285
+ config.providers[provider.id].active = true;
286
+ config.providers[provider.id].configuredAt = new Date().toISOString();
287
+
288
+ if (saveConfig(config)) {
289
+ console.log(chalk.green(` ✓ ${provider.name} connected via CLIProxy.`));
290
+ } else {
291
+ console.log(chalk.red(' Failed to save config.'));
292
+ }
293
+ await prompts.waitForEnter();
294
+ continue;
295
+ }
296
+
297
+ if (choice === '2') {
298
+ // API Key connection
299
+ console.log();
300
+ console.log(chalk.yellow(` Enter your ${provider.name} API key:`));
301
+ console.log(chalk.gray(' (Press Enter to cancel)'));
302
+ console.log();
303
+
304
+ const apiKey = await prompts.textInput(chalk.cyan(' API Key: '), true);
305
+
306
+ if (!apiKey || apiKey.trim() === '') {
307
+ console.log(chalk.gray(' Cancelled.'));
308
+ await prompts.waitForEnter();
309
+ continue;
310
+ }
311
+
312
+ if (apiKey.length < 20) {
313
+ console.log(chalk.red(' Invalid API key format (too short).'));
314
+ await prompts.waitForEnter();
315
+ continue;
316
+ }
317
+
318
+ // Deactivate all other providers
319
+ Object.keys(config.providers).forEach(id => {
320
+ if (config.providers[id]) config.providers[id].active = false;
321
+ });
322
+
323
+ if (!config.providers[provider.id]) config.providers[provider.id] = {};
324
+ config.providers[provider.id].connectionType = 'apikey';
325
+ config.providers[provider.id].apiKey = apiKey.trim();
326
+ config.providers[provider.id].active = true;
327
+ config.providers[provider.id].configuredAt = new Date().toISOString();
328
+
329
+ if (saveConfig(config)) {
330
+ console.log(chalk.green(` ✓ ${provider.name} connected via API Key.`));
331
+ } else {
332
+ console.log(chalk.red(' Failed to save config.'));
333
+ }
334
+ await prompts.waitForEnter();
335
+ continue;
336
+ }
337
+ }
338
+
339
+ return config;
340
+ };
341
+
342
+ /**
343
+ * Get active AI provider config
344
+ * @returns {Object|null} Active provider config or null
345
+ */
346
+ const getActiveProvider = () => {
347
+ const config = loadConfig();
348
+ for (const provider of AI_PROVIDERS) {
349
+ const providerConfig = config.providers[provider.id];
350
+ if (providerConfig && providerConfig.active) {
351
+ return {
352
+ id: provider.id,
353
+ name: provider.name,
354
+ connectionType: providerConfig.connectionType,
355
+ apiKey: providerConfig.apiKey || null
356
+ };
357
+ }
358
+ }
359
+ return null;
360
+ };
361
+
362
+ /**
363
+ * Count active AI agents
364
+ * @returns {number} Number of active agents (0 or 1)
365
+ */
366
+ const getActiveAgentCount = () => {
367
+ const active = getActiveProvider();
368
+ return active ? 1 : 0;
369
+ };
370
+
371
+ /**
372
+ * Main AI Agents menu
373
+ */
374
+ const aiAgentsMenu = async () => {
375
+ let config = loadConfig();
376
+ const boxWidth = getLogoWidth();
377
+
378
+ while (true) {
379
+ console.clear();
380
+ drawProvidersTable(config, boxWidth);
381
+
382
+ const input = await prompts.textInput(chalk.cyan('Select provider: '));
383
+ const choice = (input || '').toLowerCase().trim();
384
+
385
+ if (choice === 'b' || choice === '') {
386
+ break;
387
+ }
388
+
389
+ const num = parseInt(choice);
390
+ if (!isNaN(num) && num >= 1 && num <= AI_PROVIDERS.length) {
391
+ config = await handleProviderConfig(AI_PROVIDERS[num - 1], config);
392
+ continue;
393
+ }
394
+
395
+ console.log(chalk.red(' Invalid option.'));
396
+ await new Promise(r => setTimeout(r, 1000));
397
+ }
398
+ };
399
+
400
+ module.exports = {
401
+ aiAgentsMenu,
402
+ getActiveProvider,
403
+ getActiveAgentCount,
404
+ loadConfig,
405
+ saveConfig,
406
+ AI_PROVIDERS
407
+ };