hedgequantx 2.7.19 → 2.7.21

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.19",
3
+ "version": "2.7.21",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -10,8 +10,10 @@ const os = require('os');
10
10
  const path = require('path');
11
11
  const fs = require('fs');
12
12
 
13
+ const ora = require('ora');
13
14
  const { getLogoWidth, centerText, visibleLength } = require('../ui');
14
15
  const { prompts } = require('../utils');
16
+ const { fetchModelsFromApi } = require('./ai-models');
15
17
 
16
18
  // Config file path
17
19
  const CONFIG_DIR = path.join(os.homedir(), '.hqx');
@@ -63,7 +65,7 @@ const saveConfig = (config) => {
63
65
  };
64
66
 
65
67
  /**
66
- * Mask API key for display (show first 8 and last 4 chars)
68
+ * Mask API key for display
67
69
  * @param {string} key - API key
68
70
  * @returns {string} Masked key
69
71
  */
@@ -73,71 +75,107 @@ const maskKey = (key) => {
73
75
  };
74
76
 
75
77
  /**
76
- * Draw the main providers selection table (2 columns)
77
- * @param {Object} config - Current config
78
- * @param {number} boxWidth - Box width
78
+ * Draw a 2-column row
79
79
  */
80
- const drawProvidersTable = (config, boxWidth) => {
81
- const W = boxWidth - 2;
80
+ const draw2ColRow = (leftText, rightText, W) => {
82
81
  const col1Width = Math.floor(W / 2);
83
82
  const col2Width = W - col1Width;
84
-
85
- // Header
83
+ const leftLen = visibleLength(leftText);
84
+ const leftPad = col1Width - leftLen;
85
+ const leftPadL = Math.floor(leftPad / 2);
86
+ const rightLen = visibleLength(rightText || '');
87
+ const rightPad = col2Width - rightLen;
88
+ const rightPadL = Math.floor(rightPad / 2);
89
+ console.log(
90
+ chalk.cyan('║') +
91
+ ' '.repeat(leftPadL) + leftText + ' '.repeat(leftPad - leftPadL) +
92
+ ' '.repeat(rightPadL) + (rightText || '') + ' '.repeat(rightPad - rightPadL) +
93
+ chalk.cyan('║')
94
+ );
95
+ };
96
+
97
+ /**
98
+ * Draw 2-column table
99
+ */
100
+ const draw2ColTable = (title, titleColor, items, backText, W) => {
86
101
  console.log(chalk.cyan('╔' + '═'.repeat(W) + '╗'));
87
- console.log(chalk.cyan('║') + chalk.yellow.bold(centerText('AI AGENTS CONFIGURATION', W)) + chalk.cyan('║'));
102
+ console.log(chalk.cyan('║') + titleColor(centerText(title, W)) + chalk.cyan('║'));
88
103
  console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
89
104
 
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);
105
+ const rows = Math.ceil(items.length / 2);
95
106
  for (let row = 0; row < rows; row++) {
96
- const leftIdx = row;
97
- const rightIdx = row + rows;
107
+ const left = items[row];
108
+ const right = items[row + rows];
109
+ draw2ColRow(left || '', right || '', W);
110
+ }
111
+
112
+ console.log(chalk.cyan('╠' + '─'.repeat(W) + '╣'));
113
+ console.log(chalk.cyan('║') + chalk.red(centerText(backText, W)) + chalk.cyan('║'));
114
+ console.log(chalk.cyan('╚' + '═'.repeat(W) + '╝'));
115
+ };
116
+
117
+ /**
118
+ * Draw providers table
119
+ */
120
+ const drawProvidersTable = (config, boxWidth) => {
121
+ const W = boxWidth - 2;
122
+ const items = AI_PROVIDERS.map((p, i) => {
123
+ const status = config.providers[p.id]?.active ? chalk.green(' ●') : '';
124
+ return chalk.cyan(`[${i + 1}]`) + ' ' + chalk[p.color](p.name) + status;
125
+ });
126
+ draw2ColTable('AI AGENTS CONFIGURATION', chalk.yellow.bold, items, '[B] Back to Menu', W);
127
+ };
128
+
129
+ /**
130
+ * Draw models table
131
+ */
132
+ const drawModelsTable = (provider, models, boxWidth) => {
133
+ const W = boxWidth - 2;
134
+ const items = models.map((m, i) => chalk.cyan(`[${i + 1}]`) + ' ' + chalk.white(m.name));
135
+ draw2ColTable(`${provider.name.toUpperCase()} - MODELS`, chalk[provider.color].bold, items, '[B] Back', W);
136
+ };
137
+
138
+ /**
139
+ * Select a model for a provider (fetches from API)
140
+ * @param {Object} provider - Provider object
141
+ * @param {string} apiKey - API key for fetching models
142
+ * @returns {Object|null} Selected model or null if cancelled/failed
143
+ */
144
+ const selectModel = async (provider, apiKey) => {
145
+ const boxWidth = getLogoWidth();
146
+
147
+ // Fetch models from API
148
+ const spinner = ora({ text: 'Fetching models from API...', color: 'yellow' }).start();
149
+ const result = await fetchModelsFromApi(provider.id, apiKey);
150
+
151
+ if (!result.success || result.models.length === 0) {
152
+ spinner.fail(result.error || 'No models available');
153
+ await prompts.waitForEnter();
154
+ return null;
155
+ }
156
+
157
+ spinner.succeed(`Found ${result.models.length} models`);
158
+ const models = result.models;
159
+
160
+ while (true) {
161
+ console.clear();
162
+ drawModelsTable(provider, models, boxWidth);
98
163
 
99
- const leftProvider = AI_PROVIDERS[leftIdx];
100
- const rightProvider = AI_PROVIDERS[rightIdx];
164
+ const input = await prompts.textInput(chalk.cyan('Select model: '));
165
+ const choice = (input || '').toLowerCase().trim();
101
166
 
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;
167
+ if (choice === 'b' || choice === '') {
168
+ return null;
169
+ }
112
170
 
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;
171
+ const num = parseInt(choice);
172
+ if (!isNaN(num) && num >= 1 && num <= models.length) {
173
+ return models[num - 1];
127
174
  }
128
175
 
129
- console.log(
130
- chalk.cyan('║') +
131
- ' '.repeat(leftPadL) + leftText + ' '.repeat(leftPadR) +
132
- ' '.repeat(rightPadL) + rightText + ' '.repeat(rightPadR) +
133
- chalk.cyan('║')
134
- );
176
+ console.log(chalk.red(' Invalid option.'));
177
+ await new Promise(r => setTimeout(r, 1000));
135
178
  }
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
179
  };
142
180
 
143
181
  /**
@@ -213,11 +251,8 @@ const drawProviderWindow = (provider, config, boxWidth) => {
213
251
  let statusText = '';
214
252
  if (providerConfig.active) {
215
253
  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
- }
254
+ const modelName = providerConfig.modelName || 'N/A';
255
+ statusText = chalk.green('● ACTIVE') + chalk.gray(' Model: ') + chalk.yellow(modelName) + chalk.gray(' via ') + chalk.cyan(connType);
221
256
  } else if (providerConfig.apiKey || providerConfig.connectionType) {
222
257
  statusText = chalk.yellow('● CONFIGURED') + chalk.gray(' (not active)');
223
258
  } else {
@@ -269,10 +304,10 @@ const handleProviderConfig = async (provider, config) => {
269
304
  }
270
305
 
271
306
  if (choice === '1') {
272
- // CLIProxy connection
307
+ // CLIProxy connection - models will be fetched via proxy
273
308
  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.)'));
309
+ console.log(chalk.cyan(' CLIProxy uses your paid plan subscription.'));
310
+ console.log(chalk.gray(' Model selection will be available after connecting.'));
276
311
  console.log();
277
312
 
278
313
  // Deactivate all other providers
@@ -282,6 +317,8 @@ const handleProviderConfig = async (provider, config) => {
282
317
 
283
318
  if (!config.providers[provider.id]) config.providers[provider.id] = {};
284
319
  config.providers[provider.id].connectionType = 'cliproxy';
320
+ config.providers[provider.id].modelId = null;
321
+ config.providers[provider.id].modelName = 'N/A';
285
322
  config.providers[provider.id].active = true;
286
323
  config.providers[provider.id].configuredAt = new Date().toISOString();
287
324
 
@@ -295,9 +332,9 @@ const handleProviderConfig = async (provider, config) => {
295
332
  }
296
333
 
297
334
  if (choice === '2') {
298
- // API Key connection
299
- console.log();
300
- console.log(chalk.yellow(` Enter your ${provider.name} API key:`));
335
+ // API Key connection - get key first, then fetch models
336
+ console.clear();
337
+ console.log(chalk.yellow(`\n Enter your ${provider.name} API key:`));
301
338
  console.log(chalk.gray(' (Press Enter to cancel)'));
302
339
  console.log();
303
340
 
@@ -315,6 +352,10 @@ const handleProviderConfig = async (provider, config) => {
315
352
  continue;
316
353
  }
317
354
 
355
+ // Fetch models from API with the provided key
356
+ const selectedModel = await selectModel(provider, apiKey.trim());
357
+ if (!selectedModel) continue;
358
+
318
359
  // Deactivate all other providers
319
360
  Object.keys(config.providers).forEach(id => {
320
361
  if (config.providers[id]) config.providers[id].active = false;
@@ -323,13 +364,16 @@ const handleProviderConfig = async (provider, config) => {
323
364
  if (!config.providers[provider.id]) config.providers[provider.id] = {};
324
365
  config.providers[provider.id].connectionType = 'apikey';
325
366
  config.providers[provider.id].apiKey = apiKey.trim();
367
+ config.providers[provider.id].modelId = selectedModel.id;
368
+ config.providers[provider.id].modelName = selectedModel.name;
326
369
  config.providers[provider.id].active = true;
327
370
  config.providers[provider.id].configuredAt = new Date().toISOString();
328
371
 
329
372
  if (saveConfig(config)) {
330
- console.log(chalk.green(` ✓ ${provider.name} connected via API Key.`));
373
+ console.log(chalk.green(`\n ✓ ${provider.name} connected via API Key.`));
374
+ console.log(chalk.cyan(` Model: ${selectedModel.name}`));
331
375
  } else {
332
- console.log(chalk.red(' Failed to save config.'));
376
+ console.log(chalk.red('\n Failed to save config.'));
333
377
  }
334
378
  await prompts.waitForEnter();
335
379
  continue;
@@ -352,7 +396,9 @@ const getActiveProvider = () => {
352
396
  id: provider.id,
353
397
  name: provider.name,
354
398
  connectionType: providerConfig.connectionType,
355
- apiKey: providerConfig.apiKey || null
399
+ apiKey: providerConfig.apiKey || null,
400
+ modelId: providerConfig.modelId || null,
401
+ modelName: providerConfig.modelName || null
356
402
  };
357
403
  }
358
404
  }
@@ -0,0 +1,224 @@
1
+ /**
2
+ * AI Models - Fetch from provider APIs
3
+ *
4
+ * Models are fetched dynamically from each provider's API.
5
+ * No hardcoded model lists - data comes from real APIs only.
6
+ */
7
+
8
+ const https = require('https');
9
+
10
+ /**
11
+ * API endpoints for fetching models
12
+ */
13
+ const API_ENDPOINTS = {
14
+ anthropic: 'https://api.anthropic.com/v1/models',
15
+ openai: 'https://api.openai.com/v1/models',
16
+ google: 'https://generativelanguage.googleapis.com/v1/models',
17
+ mistral: 'https://api.mistral.ai/v1/models',
18
+ groq: 'https://api.groq.com/openai/v1/models',
19
+ xai: 'https://api.x.ai/v1/models',
20
+ perplexity: 'https://api.perplexity.ai/models',
21
+ openrouter: 'https://openrouter.ai/api/v1/models',
22
+ };
23
+
24
+ /**
25
+ * Make HTTPS request
26
+ * @param {string} url - API URL
27
+ * @param {Object} headers - Request headers
28
+ * @param {number} timeout - Timeout in ms (default 60000 per RULES.md #15)
29
+ * @returns {Promise<Object>} Response data
30
+ */
31
+ const fetchApi = (url, headers = {}, timeout = 60000) => {
32
+ return new Promise((resolve, reject) => {
33
+ const urlObj = new URL(url);
34
+ const options = {
35
+ hostname: urlObj.hostname,
36
+ path: urlObj.pathname + urlObj.search,
37
+ method: 'GET',
38
+ headers: {
39
+ 'Content-Type': 'application/json',
40
+ ...headers
41
+ },
42
+ timeout
43
+ };
44
+
45
+ const req = https.request(options, (res) => {
46
+ let data = '';
47
+ res.on('data', chunk => data += chunk);
48
+ res.on('end', () => {
49
+ try {
50
+ if (res.statusCode >= 200 && res.statusCode < 300) {
51
+ resolve({ success: true, data: JSON.parse(data) });
52
+ } else {
53
+ resolve({ success: false, error: `HTTP ${res.statusCode}` });
54
+ }
55
+ } catch (error) {
56
+ resolve({ success: false, error: 'Invalid JSON response' });
57
+ }
58
+ });
59
+ });
60
+
61
+ req.on('error', (error) => {
62
+ resolve({ success: false, error: error.message });
63
+ });
64
+
65
+ req.on('timeout', () => {
66
+ req.destroy();
67
+ resolve({ success: false, error: 'Request timeout' });
68
+ });
69
+
70
+ req.end();
71
+ });
72
+ };
73
+
74
+ /**
75
+ * Get auth headers for provider
76
+ * @param {string} providerId - Provider ID
77
+ * @param {string} apiKey - API key
78
+ * @returns {Object} Headers object
79
+ */
80
+ const getAuthHeaders = (providerId, apiKey) => {
81
+ if (!apiKey) return {};
82
+
83
+ switch (providerId) {
84
+ case 'anthropic':
85
+ return { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' };
86
+ case 'openai':
87
+ case 'groq':
88
+ case 'xai':
89
+ case 'perplexity':
90
+ case 'openrouter':
91
+ return { 'Authorization': `Bearer ${apiKey}` };
92
+ case 'google':
93
+ return {}; // Google uses query param
94
+ case 'mistral':
95
+ return { 'Authorization': `Bearer ${apiKey}` };
96
+ default:
97
+ return { 'Authorization': `Bearer ${apiKey}` };
98
+ }
99
+ };
100
+
101
+ /**
102
+ * Parse models response based on provider
103
+ * @param {string} providerId - Provider ID
104
+ * @param {Object} data - API response data
105
+ * @returns {Array} Parsed models list
106
+ */
107
+ const parseModelsResponse = (providerId, data) => {
108
+ if (!data) return [];
109
+
110
+ try {
111
+ switch (providerId) {
112
+ case 'anthropic':
113
+ // Anthropic returns { data: [{ id, display_name, ... }] }
114
+ return (data.data || []).map(m => ({
115
+ id: m.id,
116
+ name: m.display_name || m.id
117
+ }));
118
+
119
+ case 'openai':
120
+ case 'groq':
121
+ case 'xai':
122
+ // OpenAI format: { data: [{ id, ... }] }
123
+ return (data.data || [])
124
+ .filter(m => m.id && !m.id.includes('whisper') && !m.id.includes('tts') && !m.id.includes('dall-e'))
125
+ .map(m => ({
126
+ id: m.id,
127
+ name: m.id
128
+ }));
129
+
130
+ case 'google':
131
+ // Google format: { models: [{ name, displayName, ... }] }
132
+ return (data.models || []).map(m => ({
133
+ id: m.name?.replace('models/', '') || m.name,
134
+ name: m.displayName || m.name
135
+ }));
136
+
137
+ case 'mistral':
138
+ // Mistral format: { data: [{ id, ... }] }
139
+ return (data.data || []).map(m => ({
140
+ id: m.id,
141
+ name: m.id
142
+ }));
143
+
144
+ case 'perplexity':
145
+ // Perplexity format varies
146
+ return (data.models || data.data || []).map(m => ({
147
+ id: m.id || m.model,
148
+ name: m.id || m.model
149
+ }));
150
+
151
+ case 'openrouter':
152
+ // OpenRouter format: { data: [{ id, name, ... }] }
153
+ return (data.data || []).map(m => ({
154
+ id: m.id,
155
+ name: m.name || m.id
156
+ }));
157
+
158
+ default:
159
+ return [];
160
+ }
161
+ } catch (error) {
162
+ return [];
163
+ }
164
+ };
165
+
166
+ /**
167
+ * Fetch models from provider API
168
+ * @param {string} providerId - Provider ID
169
+ * @param {string} apiKey - API key (required for most providers)
170
+ * @returns {Promise<Object>} { success, models, error }
171
+ */
172
+ const fetchModelsFromApi = async (providerId, apiKey) => {
173
+ const endpoint = API_ENDPOINTS[providerId];
174
+ if (!endpoint) {
175
+ return { success: false, models: [], error: 'Unknown provider' };
176
+ }
177
+
178
+ // Build URL (Google needs API key in query)
179
+ let url = endpoint;
180
+ if (providerId === 'google' && apiKey) {
181
+ url += `?key=${apiKey}`;
182
+ }
183
+
184
+ const headers = getAuthHeaders(providerId, apiKey);
185
+ const result = await fetchApi(url, headers);
186
+
187
+ if (!result.success) {
188
+ return { success: false, models: [], error: result.error };
189
+ }
190
+
191
+ const models = parseModelsResponse(providerId, result.data);
192
+
193
+ if (models.length === 0) {
194
+ return { success: false, models: [], error: 'No models returned' };
195
+ }
196
+
197
+ return { success: true, models, error: null };
198
+ };
199
+
200
+ /**
201
+ * Get models for a provider - returns empty, use fetchModelsFromApi
202
+ * @param {string} providerId - Provider ID
203
+ * @returns {Array} Empty array
204
+ */
205
+ const getModelsForProvider = (providerId) => {
206
+ return [];
207
+ };
208
+
209
+ /**
210
+ * Get model by ID - returns null, use API data
211
+ * @param {string} providerId - Provider ID
212
+ * @param {string} modelId - Model ID
213
+ * @returns {null} Always null
214
+ */
215
+ const getModelById = (providerId, modelId) => {
216
+ return null;
217
+ };
218
+
219
+ module.exports = {
220
+ fetchModelsFromApi,
221
+ getModelsForProvider,
222
+ getModelById,
223
+ API_ENDPOINTS
224
+ };