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 +1 -1
- package/src/menus/ai-agent.js +215 -45
- package/src/services/ai/client.js +36 -23
package/package.json
CHANGED
package/src/menus/ai-agent.js
CHANGED
|
@@ -782,7 +782,7 @@ const getOAuthConfig = (providerId) => {
|
|
|
782
782
|
|
|
783
783
|
/**
|
|
784
784
|
* Setup OAuth connection for any provider with OAuth support
|
|
785
|
-
*
|
|
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
|
|
796
|
-
return await
|
|
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
|
-
|
|
904
|
+
fetchError = e.message;
|
|
904
905
|
}
|
|
905
906
|
|
|
906
|
-
|
|
907
|
-
|
|
907
|
+
// RULE: Models MUST come from API - no hardcoded fallback
|
|
908
908
|
if (!models || models.length === 0) {
|
|
909
|
-
spinner.
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
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
|
-
*
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
954
|
-
|
|
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
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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
|
-
//
|
|
1268
|
-
|
|
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(
|
|
1440
|
+
pollSpinner.succeed(`Found ${models.length} models`);
|
|
1271
1441
|
|
|
1272
|
-
// Let user select model
|
|
1273
|
-
const selectedModel = await selectModelFromList(
|
|
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<
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
696
|
-
|
|
697
|
-
'
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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
|
|