hedgequantx 2.6.100 → 2.6.102

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.6.100",
3
+ "version": "2.6.102",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -782,7 +782,7 @@ const getOAuthConfig = (providerId) => {
782
782
 
783
783
  /**
784
784
  * Setup OAuth connection for any provider with OAuth support
785
- * Unified flow: copy code from URL (works on local and VPS)
785
+ * Uses CLIProxyAPI for proper OAuth handling and API access
786
786
  */
787
787
  const setupOAuthConnection = async (provider) => {
788
788
  const config = getOAuthConfig(provider.id);
@@ -792,8 +792,8 @@ const setupOAuthConnection = async (provider) => {
792
792
  return await selectProviderOption(provider);
793
793
  }
794
794
 
795
- // Use unified manual flow for all providers and environments
796
- return await setupRemoteOAuth(provider, config);
795
+ // Use CLIProxyAPI for OAuth flow - it handles token exchange and API calls
796
+ return await setupProxyOAuth(provider, config);
797
797
  };
798
798
 
799
799
  /**
@@ -892,35 +892,42 @@ const setupRemoteOAuth = async (provider, config) => {
892
892
  };
893
893
 
894
894
  // Try to fetch models with the new token
895
- spinner.text = 'Fetching available models...';
895
+ spinner.text = 'Fetching available models from API...';
896
896
  spinner.start();
897
897
 
898
898
  let models = [];
899
+ let fetchError = null;
899
900
  try {
900
901
  const { fetchModelsWithOAuth } = require('../services/ai/client');
901
902
  models = await fetchModelsWithOAuth(provider.id, result.access);
902
903
  } catch (e) {
903
- // API failed - will prompt for manual input
904
+ fetchError = e.message;
904
905
  }
905
906
 
906
- let selectedModel;
907
-
907
+ // RULE: Models MUST come from API - no hardcoded fallback
908
908
  if (!models || models.length === 0) {
909
- spinner.warn('Could not fetch models from API');
910
-
911
- // Prompt user to enter model name manually
912
- selectedModel = await promptForModelName(config.name);
913
- if (!selectedModel) {
914
- return await selectProviderOption(provider);
915
- }
916
- } else {
917
- spinner.succeed(`Found ${models.length} models`);
918
-
919
- // Let user select model from list
920
- selectedModel = await selectModelFromList(models, config.name);
921
- if (!selectedModel) {
922
- return await selectProviderOption(provider);
909
+ spinner.fail('Could not fetch models from API');
910
+ console.log();
911
+ console.log(chalk.red(' ERROR: Unable to retrieve models from provider API'));
912
+ console.log(chalk.white(' Possible causes:'));
913
+ console.log(chalk.gray(' - OAuth token may not have permission to list models'));
914
+ console.log(chalk.gray(' - Network issue or API temporarily unavailable'));
915
+ console.log(chalk.gray(' - Provider API may have changed'));
916
+ if (fetchError) {
917
+ console.log(chalk.gray(` - Error: ${fetchError}`));
923
918
  }
919
+ console.log();
920
+ console.log(chalk.yellow(' Please try again or use API Key authentication instead.'));
921
+ await prompts.waitForEnter();
922
+ return await selectProviderOption(provider);
923
+ }
924
+
925
+ spinner.succeed(`Found ${models.length} models`);
926
+
927
+ // Let user select model from list
928
+ const selectedModel = await selectModelFromList(models, config.name);
929
+ if (!selectedModel) {
930
+ return await selectProviderOption(provider);
924
931
  }
925
932
 
926
933
  // Add agent
@@ -938,32 +945,172 @@ const setupRemoteOAuth = async (provider, config) => {
938
945
  return await aiAgentMenu();
939
946
  };
940
947
 
948
+ // NOTE: promptForModelName was removed - models MUST come from API (RULES.md)
949
+
941
950
  /**
942
- * Prompt user to enter model name manually when API doesn't return models
951
+ * Setup OAuth using CLIProxyAPI
952
+ * CLIProxyAPI handles OAuth flow, token storage, and API calls
953
+ * Models are fetched from CLIProxyAPI /v1/models endpoint
943
954
  */
944
- const promptForModelName = async (providerName) => {
955
+ const setupProxyOAuth = async (provider, config) => {
956
+ const boxWidth = getLogoWidth();
957
+ const W = boxWidth - 2;
958
+
959
+ const makeLine = (content) => {
960
+ const plainLen = content.replace(/\x1b\[[0-9;]*m/g, '').length;
961
+ const padding = W - plainLen;
962
+ return chalk.cyan('║') + ' ' + content + ' '.repeat(Math.max(0, padding - 1)) + chalk.cyan('║');
963
+ };
964
+
965
+ // Step 1: Ensure CLIProxyAPI is installed and running
966
+ const spinner = ora({ text: 'Setting up CLIProxyAPI...', color: 'cyan' }).start();
967
+
968
+ try {
969
+ await proxyManager.ensureRunning();
970
+ spinner.succeed('CLIProxyAPI ready');
971
+ } catch (error) {
972
+ spinner.fail(`Failed to start CLIProxyAPI: ${error.message}`);
973
+ console.log();
974
+ console.log(chalk.yellow(' CLIProxyAPI is required for OAuth authentication.'));
975
+ console.log(chalk.gray(' It will be downloaded automatically on first use.'));
976
+ console.log();
977
+ await prompts.waitForEnter();
978
+ return await selectProviderOption(provider);
979
+ }
980
+
981
+ // Step 2: Get OAuth URL from CLIProxyAPI
982
+ spinner.text = 'Getting authorization URL...';
983
+ spinner.start();
984
+
985
+ let authUrl, authState;
986
+ try {
987
+ const authInfo = await proxyManager.getAuthUrl(provider.id);
988
+ authUrl = authInfo.url;
989
+ authState = authInfo.state;
990
+ spinner.succeed('Authorization URL ready');
991
+ } catch (error) {
992
+ spinner.fail(`Failed to get auth URL: ${error.message}`);
993
+ await prompts.waitForEnter();
994
+ return await selectProviderOption(provider);
995
+ }
996
+
997
+ // Step 3: Show instructions to user
998
+ console.clear();
999
+ displayBanner();
1000
+ drawBoxHeaderContinue(`CONNECT ${config.name}`, boxWidth);
1001
+
1002
+ console.log(makeLine(chalk.yellow('CONNECT YOUR ACCOUNT')));
1003
+ console.log(makeLine(''));
1004
+ console.log(makeLine(chalk.white('1. OPEN THE LINK BELOW IN YOUR BROWSER')));
1005
+ console.log(makeLine(''));
1006
+ console.log(makeLine(chalk.white(`2. LOGIN WITH YOUR ${config.accountName.toUpperCase()} ACCOUNT`)));
1007
+ console.log(makeLine(''));
1008
+ console.log(makeLine(chalk.white('3. CLICK "AUTHORIZE"')));
1009
+ console.log(makeLine(''));
1010
+ console.log(makeLine(chalk.green('4. WAIT FOR CONFIRMATION HERE')));
1011
+ console.log(makeLine(chalk.white(' (The page will close automatically)')));
1012
+ console.log(makeLine(''));
1013
+
1014
+ drawBoxFooter(boxWidth);
1015
+
1016
+ // Display URL
1017
+ console.log();
1018
+ console.log(chalk.yellow(' OPEN THIS URL IN YOUR BROWSER:'));
945
1019
  console.log();
946
- console.log(chalk.yellow(' Could not fetch models from API.'));
947
- console.log(chalk.white(' Please enter the model name manually.'));
948
- console.log(chalk.gray(' (Check provider docs for available models)'));
1020
+ console.log(chalk.cyan(` ${authUrl}`));
949
1021
  console.log();
950
1022
 
951
- const modelName = await prompts.textInput(chalk.cyan('MODEL NAME:'));
1023
+ // Try to open browser automatically
1024
+ const browserOpened = await openBrowser(authUrl);
1025
+ if (browserOpened) {
1026
+ console.log(chalk.gray(' (Browser opened automatically)'));
1027
+ } else {
1028
+ console.log(chalk.gray(' (Copy and paste the URL in your browser)'));
1029
+ }
1030
+ console.log();
952
1031
 
953
- if (!modelName || modelName.trim() === '' || modelName.trim() === '<') {
954
- return null;
1032
+ // Step 4: Wait for OAuth callback
1033
+ const waitSpinner = ora({ text: 'Waiting for authorization...', color: 'cyan' }).start();
1034
+
1035
+ try {
1036
+ await proxyManager.waitForAuth(authState, 300000, (status) => {
1037
+ waitSpinner.text = status;
1038
+ });
1039
+ waitSpinner.succeed('Authorization successful!');
1040
+ } catch (error) {
1041
+ waitSpinner.fail(`Authorization failed: ${error.message}`);
1042
+ await prompts.waitForEnter();
1043
+ return await selectProviderOption(provider);
955
1044
  }
956
1045
 
957
- return modelName.trim();
958
- };
959
-
960
- /**
961
- * Setup OAuth via Manual Code Entry (unified flow for local and VPS)
962
- * User copies the authorization code from the URL or page
963
- */
964
- const setupProxyOAuth = async (provider, config) => {
965
- // Use the same flow as VPS - it works everywhere
966
- return await setupRemoteOAuth(provider, config);
1046
+ // Step 5: Fetch models from CLIProxyAPI
1047
+ waitSpinner.text = 'Fetching available models from API...';
1048
+ waitSpinner.start();
1049
+
1050
+ let models = [];
1051
+ try {
1052
+ models = await proxyManager.getModels();
1053
+ } catch (error) {
1054
+ waitSpinner.fail(`Failed to fetch models: ${error.message}`);
1055
+ await prompts.waitForEnter();
1056
+ return await selectProviderOption(provider);
1057
+ }
1058
+
1059
+ // Filter models for this provider
1060
+ const providerPrefixes = {
1061
+ anthropic: ['claude'],
1062
+ openai: ['gpt', 'o1', 'o3', 'o4'],
1063
+ gemini: ['gemini'],
1064
+ qwen: ['qwen'],
1065
+ iflow: ['deepseek', 'kimi', 'glm']
1066
+ };
1067
+
1068
+ const prefixes = providerPrefixes[provider.id] || [];
1069
+ const filteredModels = models.filter(m => {
1070
+ const modelLower = m.toLowerCase();
1071
+ return prefixes.some(p => modelLower.includes(p));
1072
+ });
1073
+
1074
+ // Use filtered models if available, otherwise use all models
1075
+ const availableModels = filteredModels.length > 0 ? filteredModels : models;
1076
+
1077
+ if (!availableModels || availableModels.length === 0) {
1078
+ waitSpinner.fail('No models available');
1079
+ console.log();
1080
+ console.log(chalk.red(' ERROR: No models found for this provider'));
1081
+ console.log(chalk.gray(' Make sure your subscription is active.'));
1082
+ console.log();
1083
+ await prompts.waitForEnter();
1084
+ return await selectProviderOption(provider);
1085
+ }
1086
+
1087
+ waitSpinner.succeed(`Found ${availableModels.length} models`);
1088
+
1089
+ // Step 6: Let user select model
1090
+ const selectedModel = await selectModelFromList(availableModels, config.name);
1091
+ if (!selectedModel) {
1092
+ return await selectProviderOption(provider);
1093
+ }
1094
+
1095
+ // Step 7: Save agent config (CLIProxyAPI stores the actual tokens)
1096
+ // We just save a reference to use the proxy
1097
+ const credentials = {
1098
+ useProxy: true,
1099
+ provider: provider.id
1100
+ };
1101
+
1102
+ try {
1103
+ await aiService.addAgent(provider.id, config.optionId, credentials, selectedModel, config.agentName);
1104
+
1105
+ console.log(chalk.green(`\n CONNECTED TO ${config.name}`));
1106
+ console.log(chalk.white(` MODEL: ${selectedModel}`));
1107
+ console.log(chalk.white(' UNLIMITED USAGE WITH YOUR SUBSCRIPTION'));
1108
+ } catch (error) {
1109
+ console.log(chalk.red(`\n FAILED TO SAVE: ${error.message}`));
1110
+ }
1111
+
1112
+ await prompts.waitForEnter();
1113
+ return await aiAgentMenu();
967
1114
  };
968
1115
 
969
1116
  /**
@@ -1252,7 +1399,7 @@ const setupDeviceFlowOAuth = async (provider, config) => {
1252
1399
  return await selectProviderOption(provider);
1253
1400
  }
1254
1401
 
1255
- pollSpinner.text = 'FETCHING AVAILABLE MODELS...';
1402
+ pollSpinner.text = 'FETCHING AVAILABLE MODELS FROM API...';
1256
1403
 
1257
1404
  // Store OAuth credentials
1258
1405
  const credentials = {
@@ -1264,13 +1411,36 @@ const setupDeviceFlowOAuth = async (provider, config) => {
1264
1411
  }
1265
1412
  };
1266
1413
 
1267
- // Default models for Qwen
1268
- const defaultModels = ['qwen3-coder-plus', 'qwen3-235b', 'qwen-max', 'qwen-plus', 'qwen-turbo'];
1414
+ // Fetch models from API - NO hardcoded fallback (RULES.md)
1415
+ let models = [];
1416
+ let fetchError = null;
1417
+ try {
1418
+ const { fetchModelsWithOAuth } = require('../services/ai/client');
1419
+ models = await fetchModelsWithOAuth(provider.id, pollResult.access);
1420
+ } catch (e) {
1421
+ fetchError = e.message;
1422
+ }
1423
+
1424
+ if (!models || models.length === 0) {
1425
+ pollSpinner.fail('Could not fetch models from API');
1426
+ console.log();
1427
+ console.log(chalk.red(' ERROR: Unable to retrieve models from provider API'));
1428
+ console.log(chalk.white(' Possible causes:'));
1429
+ console.log(chalk.gray(' - OAuth token may not have permission to list models'));
1430
+ console.log(chalk.gray(' - Network issue or API temporarily unavailable'));
1431
+ if (fetchError) {
1432
+ console.log(chalk.gray(` - Error: ${fetchError}`));
1433
+ }
1434
+ console.log();
1435
+ console.log(chalk.yellow(' Please try again or use API Key authentication instead.'));
1436
+ await prompts.waitForEnter();
1437
+ return await selectProviderOption(provider);
1438
+ }
1269
1439
 
1270
- pollSpinner.succeed('AUTHENTICATION SUCCESSFUL');
1440
+ pollSpinner.succeed(`Found ${models.length} models`);
1271
1441
 
1272
- // Let user select model
1273
- const selectedModel = await selectModelFromList(defaultModels, config.name);
1442
+ // Let user select model from API list
1443
+ const selectedModel = await selectModelFromList(models, config.name);
1274
1444
  if (!selectedModel) {
1275
1445
  return await selectProviderOption(provider);
1276
1446
  }
@@ -525,15 +525,17 @@ const fetchAnthropicModels = async (apiKey) => {
525
525
 
526
526
  /**
527
527
  * Fetch available models from Anthropic API (OAuth auth)
528
+ *
528
529
  * @param {string} accessToken - OAuth access token
529
- * @returns {Promise<Array|null>} Array of model IDs or null on error
530
+ * @returns {Promise<Object>} { models: Array, error: string|null }
530
531
  *
531
532
  * Data source: https://api.anthropic.com/v1/models (GET with Bearer token)
533
+ * NO HARDCODED FALLBACK - models must come from API only
532
534
  */
533
535
  const fetchAnthropicModelsOAuth = async (accessToken) => {
534
- if (!accessToken) return null;
536
+ if (!accessToken) return { models: null, error: 'No access token provided' };
535
537
 
536
- const url = 'https://api.anthropic.com/v1/models';
538
+ const modelsUrl = 'https://api.anthropic.com/v1/models';
537
539
 
538
540
  const headers = {
539
541
  'Authorization': `Bearer ${accessToken}`,
@@ -542,15 +544,14 @@ const fetchAnthropicModelsOAuth = async (accessToken) => {
542
544
  };
543
545
 
544
546
  try {
545
- const response = await makeRequest(url, { method: 'GET', headers, timeout: 10000 });
547
+ const response = await makeRequest(modelsUrl, { method: 'GET', headers, timeout: 15000 });
546
548
  if (response.data && Array.isArray(response.data)) {
547
549
  const models = response.data.map(m => m.id).filter(Boolean);
548
- if (models.length > 0) return models;
550
+ if (models.length > 0) return { models, error: null };
549
551
  }
550
- return null;
552
+ return { models: null, error: 'API returned empty or invalid response' };
551
553
  } catch (error) {
552
- // OAuth token may not support /models endpoint
553
- return null;
554
+ return { models: null, error: error.message };
554
555
  }
555
556
  };
556
557
 
@@ -661,6 +662,7 @@ const fetchModelsWithOAuth = async (providerId, accessToken) => {
661
662
 
662
663
  case 'openai': {
663
664
  // Try OpenAI /v1/models endpoint with OAuth token
665
+ // NO HARDCODED FALLBACK - models must come from API only
664
666
  const openaiModels = await fetchOpenAIModels('https://api.openai.com/v1', accessToken);
665
667
  if (openaiModels && openaiModels.length > 0) {
666
668
  return openaiModels;
@@ -684,7 +686,9 @@ const fetchModelsWithOAuth = async (providerId, accessToken) => {
684
686
  .filter(Boolean);
685
687
  }
686
688
  } catch (e) {
687
- // ChatGPT backend not available
689
+ if (process.env.HQX_DEBUG) {
690
+ console.error('[DEBUG] ChatGPT backend error:', e.message);
691
+ }
688
692
  }
689
693
 
690
694
  return null;
@@ -692,21 +696,30 @@ const fetchModelsWithOAuth = async (providerId, accessToken) => {
692
696
 
693
697
  case 'gemini': {
694
698
  // Gemini OAuth - fetch from Generative Language API
695
- const geminiUrl = 'https://generativelanguage.googleapis.com/v1/models';
696
- const geminiHeaders = {
697
- 'Authorization': `Bearer ${accessToken}`
698
- };
699
- const geminiResponse = await makeRequest(geminiUrl, {
700
- method: 'GET',
701
- headers: geminiHeaders,
702
- timeout: 10000
703
- });
704
- if (geminiResponse.models && Array.isArray(geminiResponse.models)) {
705
- return geminiResponse.models
706
- .filter(m => m.supportedGenerationMethods?.includes('generateContent'))
707
- .map(m => m.name.replace('models/', ''))
708
- .filter(Boolean);
699
+ // NO HARDCODED FALLBACK - models must come from API only
700
+ try {
701
+ const geminiUrl = 'https://generativelanguage.googleapis.com/v1/models';
702
+ const geminiHeaders = {
703
+ 'Authorization': `Bearer ${accessToken}`
704
+ };
705
+ const geminiResponse = await makeRequest(geminiUrl, {
706
+ method: 'GET',
707
+ headers: geminiHeaders,
708
+ timeout: 15000
709
+ });
710
+ if (geminiResponse.models && Array.isArray(geminiResponse.models)) {
711
+ const models = geminiResponse.models
712
+ .filter(m => m.supportedGenerationMethods?.includes('generateContent'))
713
+ .map(m => m.name.replace('models/', ''))
714
+ .filter(Boolean);
715
+ if (models.length > 0) return models;
716
+ }
717
+ } catch (e) {
718
+ if (process.env.HQX_DEBUG) {
719
+ console.error('[DEBUG] Gemini models API error:', e.message);
720
+ }
709
721
  }
722
+
710
723
  return null;
711
724
  }
712
725