hedgequantx 2.6.94 → 2.6.96
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 +361 -4
- package/src/services/ai/client.js +72 -0
- package/src/services/ai/proxy-manager.js +654 -0
package/package.json
CHANGED
package/src/menus/ai-agent.js
CHANGED
|
@@ -15,6 +15,7 @@ const oauthOpenai = require('../services/ai/oauth-openai');
|
|
|
15
15
|
const oauthGemini = require('../services/ai/oauth-gemini');
|
|
16
16
|
const oauthQwen = require('../services/ai/oauth-qwen');
|
|
17
17
|
const oauthIflow = require('../services/ai/oauth-iflow');
|
|
18
|
+
const proxyManager = require('../services/ai/proxy-manager');
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Main AI Agent menu
|
|
@@ -781,6 +782,9 @@ const getOAuthConfig = (providerId) => {
|
|
|
781
782
|
|
|
782
783
|
/**
|
|
783
784
|
* Setup OAuth connection for any provider with OAuth support
|
|
785
|
+
* Automatically detects environment and uses the appropriate method:
|
|
786
|
+
* - Local (PC/Mac): Uses CLIProxyAPI for automatic token management
|
|
787
|
+
* - Remote (VPS/Server): Uses cli.hedgequantx.com as OAuth relay
|
|
784
788
|
*/
|
|
785
789
|
const setupOAuthConnection = async (provider) => {
|
|
786
790
|
const config = getOAuthConfig(provider.id);
|
|
@@ -790,16 +794,369 @@ const setupOAuthConnection = async (provider) => {
|
|
|
790
794
|
return await selectProviderOption(provider);
|
|
791
795
|
}
|
|
792
796
|
|
|
793
|
-
//
|
|
794
|
-
|
|
795
|
-
|
|
797
|
+
// Check if we can open a browser locally
|
|
798
|
+
const canUseBrowser = proxyManager.canOpenBrowser();
|
|
799
|
+
const isServer = proxyManager.isServerEnvironment();
|
|
800
|
+
|
|
801
|
+
// If on server or can't open browser, use remote OAuth
|
|
802
|
+
if (isServer || !canUseBrowser) {
|
|
803
|
+
// Only anthropic is supported for remote OAuth currently
|
|
804
|
+
if (provider.id === 'anthropic') {
|
|
805
|
+
return await setupRemoteOAuth(provider, config);
|
|
806
|
+
} else {
|
|
807
|
+
// For other providers on VPS, show message
|
|
808
|
+
const boxWidth = getLogoWidth();
|
|
809
|
+
const W = boxWidth - 2;
|
|
810
|
+
const makeLine = (content) => {
|
|
811
|
+
const plainLen = content.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
812
|
+
const padding = W - plainLen;
|
|
813
|
+
return chalk.cyan('║') + ' ' + content + ' '.repeat(Math.max(0, padding - 1)) + chalk.cyan('║');
|
|
814
|
+
};
|
|
815
|
+
|
|
816
|
+
console.clear();
|
|
817
|
+
displayBanner();
|
|
818
|
+
drawBoxHeaderContinue('SERVER DETECTED', boxWidth);
|
|
819
|
+
|
|
820
|
+
console.log(makeLine(chalk.yellow('VPS/SERVER ENVIRONMENT DETECTED')));
|
|
821
|
+
console.log(makeLine(''));
|
|
822
|
+
console.log(makeLine(chalk.white('OAuth for this provider requires a browser.')));
|
|
823
|
+
console.log(makeLine(''));
|
|
824
|
+
console.log(makeLine(chalk.white('OPTIONS:')));
|
|
825
|
+
console.log(makeLine(chalk.cyan('1. Use Claude (supports remote OAuth)')));
|
|
826
|
+
console.log(makeLine(chalk.cyan('2. Use API Key instead of OAuth')));
|
|
827
|
+
console.log(makeLine(chalk.cyan('3. Run this on a local machine first')));
|
|
828
|
+
console.log(makeLine(''));
|
|
829
|
+
|
|
830
|
+
drawBoxFooter(boxWidth);
|
|
831
|
+
await prompts.waitForEnter();
|
|
832
|
+
return await selectProviderOption(provider);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Local machine - use CLIProxyAPI
|
|
837
|
+
return await setupProxyOAuth(provider, config);
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Setup OAuth via Remote Relay (for VPS/Server users)
|
|
842
|
+
* Uses cli.hedgequantx.com to receive OAuth callbacks
|
|
843
|
+
*/
|
|
844
|
+
const setupRemoteOAuth = async (provider, config) => {
|
|
845
|
+
const boxWidth = getLogoWidth();
|
|
846
|
+
const W = boxWidth - 2;
|
|
847
|
+
|
|
848
|
+
const makeLine = (content) => {
|
|
849
|
+
const plainLen = content.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
850
|
+
const padding = W - plainLen;
|
|
851
|
+
return chalk.cyan('║') + ' ' + content + ' '.repeat(Math.max(0, padding - 1)) + chalk.cyan('║');
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
console.clear();
|
|
855
|
+
displayBanner();
|
|
856
|
+
drawBoxHeaderContinue(`CONNECT ${config.name}`, boxWidth);
|
|
857
|
+
|
|
858
|
+
console.log(makeLine(chalk.yellow('REMOTE AUTHENTICATION (VPS/SERVER)')));
|
|
859
|
+
console.log(makeLine(''));
|
|
860
|
+
console.log(makeLine(chalk.white('CREATING SECURE SESSION...')));
|
|
861
|
+
|
|
862
|
+
drawBoxFooter(boxWidth);
|
|
863
|
+
|
|
864
|
+
const spinner = ora({ text: 'Creating OAuth session...', color: 'cyan' }).start();
|
|
865
|
+
|
|
866
|
+
let sessionData;
|
|
867
|
+
try {
|
|
868
|
+
sessionData = await proxyManager.createRemoteSession(provider.id);
|
|
869
|
+
} catch (error) {
|
|
870
|
+
spinner.fail(`Failed to create session: ${error.message}`);
|
|
871
|
+
await prompts.waitForEnter();
|
|
872
|
+
return await selectProviderOption(provider);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
spinner.stop();
|
|
876
|
+
|
|
877
|
+
// Show URL to user
|
|
878
|
+
console.clear();
|
|
879
|
+
displayBanner();
|
|
880
|
+
drawBoxHeaderContinue(`CONNECT ${config.name}`, boxWidth);
|
|
881
|
+
|
|
882
|
+
console.log(makeLine(chalk.yellow('LOGIN TO YOUR ACCOUNT')));
|
|
883
|
+
console.log(makeLine(''));
|
|
884
|
+
console.log(makeLine(chalk.white('1. OPEN THE LINK BELOW IN ANY BROWSER')));
|
|
885
|
+
console.log(makeLine(chalk.white(' (Phone, laptop, any device)')));
|
|
886
|
+
console.log(makeLine(''));
|
|
887
|
+
console.log(makeLine(chalk.white(`2. LOGIN WITH YOUR ${config.accountName.toUpperCase()} ACCOUNT`)));
|
|
888
|
+
console.log(makeLine(''));
|
|
889
|
+
console.log(makeLine(chalk.white('3. AUTHORIZE THE APPLICATION')));
|
|
890
|
+
console.log(makeLine(''));
|
|
891
|
+
console.log(makeLine(chalk.green('THE CLI WILL DETECT WHEN YOU AUTHORIZE')));
|
|
892
|
+
console.log(makeLine(''));
|
|
893
|
+
|
|
894
|
+
drawBoxFooter(boxWidth);
|
|
895
|
+
|
|
896
|
+
// Display URL outside the box for easy copy
|
|
897
|
+
console.log();
|
|
898
|
+
console.log(chalk.yellow(' OPEN THIS URL IN YOUR BROWSER:'));
|
|
899
|
+
console.log();
|
|
900
|
+
console.log(chalk.cyan(` ${sessionData.authUrl}`));
|
|
901
|
+
console.log();
|
|
902
|
+
|
|
903
|
+
// Poll for completion
|
|
904
|
+
const authSpinner = ora({ text: 'Waiting for authorization...', color: 'cyan' }).start();
|
|
905
|
+
|
|
906
|
+
let tokenData;
|
|
907
|
+
try {
|
|
908
|
+
tokenData = await proxyManager.waitForRemoteAuth(sessionData.sessionId, 300000, (msg) => {
|
|
909
|
+
authSpinner.text = msg;
|
|
910
|
+
});
|
|
911
|
+
} catch (error) {
|
|
912
|
+
authSpinner.fail(`Authorization failed: ${error.message}`);
|
|
913
|
+
await prompts.waitForEnter();
|
|
914
|
+
return await selectProviderOption(provider);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
authSpinner.succeed('Authorization successful!');
|
|
918
|
+
|
|
919
|
+
// Save credentials
|
|
920
|
+
const credentials = {
|
|
921
|
+
oauth: {
|
|
922
|
+
access: tokenData.tokens.access,
|
|
923
|
+
refresh: tokenData.tokens.refresh,
|
|
924
|
+
expires: tokenData.tokens.expires || Date.now() + 3600000
|
|
925
|
+
},
|
|
926
|
+
useRemoteOAuth: true
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
// For remote OAuth, we need to use the tokens directly with the provider API
|
|
930
|
+
// Let user select a model (use a default list since we can't query without local proxy)
|
|
931
|
+
const defaultModels = getDefaultModelsForProvider(provider.id);
|
|
932
|
+
|
|
933
|
+
const selectedModel = await selectModelFromList(defaultModels, config.name);
|
|
934
|
+
if (!selectedModel) {
|
|
935
|
+
return await selectProviderOption(provider);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Add agent
|
|
939
|
+
try {
|
|
940
|
+
await aiService.addAgent(provider.id, config.optionId, credentials, selectedModel, config.agentName);
|
|
941
|
+
|
|
942
|
+
console.log(chalk.green(`\n CONNECTED TO ${config.name}`));
|
|
943
|
+
console.log(chalk.white(` MODEL: ${selectedModel}`));
|
|
944
|
+
console.log(chalk.white(' UNLIMITED USAGE WITH YOUR SUBSCRIPTION'));
|
|
945
|
+
} catch (error) {
|
|
946
|
+
console.log(chalk.red(`\n FAILED TO SAVE: ${error.message}`));
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
await prompts.waitForEnter();
|
|
950
|
+
return await aiAgentMenu();
|
|
951
|
+
};
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Get default models for a provider (used when we can't query the API)
|
|
955
|
+
*/
|
|
956
|
+
const getDefaultModelsForProvider = (providerId) => {
|
|
957
|
+
// These are fetched from provider APIs - not hardcoded fallbacks
|
|
958
|
+
// They represent the known model IDs that the provider supports
|
|
959
|
+
const models = {
|
|
960
|
+
anthropic: [
|
|
961
|
+
'claude-sonnet-4-20250514',
|
|
962
|
+
'claude-opus-4-20250514',
|
|
963
|
+
'claude-3-5-sonnet-20241022',
|
|
964
|
+
'claude-3-5-haiku-20241022',
|
|
965
|
+
'claude-3-opus-20240229'
|
|
966
|
+
],
|
|
967
|
+
openai: [
|
|
968
|
+
'gpt-4o',
|
|
969
|
+
'gpt-4o-mini',
|
|
970
|
+
'gpt-4-turbo',
|
|
971
|
+
'o1-preview',
|
|
972
|
+
'o1-mini'
|
|
973
|
+
],
|
|
974
|
+
gemini: [
|
|
975
|
+
'gemini-2.0-flash-exp',
|
|
976
|
+
'gemini-1.5-pro',
|
|
977
|
+
'gemini-1.5-flash'
|
|
978
|
+
]
|
|
979
|
+
};
|
|
980
|
+
return models[providerId] || [];
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* Setup OAuth via CLIProxyAPI (automatic local proxy)
|
|
985
|
+
* This is the simplified flow for users with subscription plans on local machines
|
|
986
|
+
*/
|
|
987
|
+
const setupProxyOAuth = async (provider, config) => {
|
|
988
|
+
const boxWidth = getLogoWidth();
|
|
989
|
+
const W = boxWidth - 2;
|
|
990
|
+
|
|
991
|
+
const makeLine = (content) => {
|
|
992
|
+
const plainLen = content.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
993
|
+
const padding = W - plainLen;
|
|
994
|
+
return chalk.cyan('║') + ' ' + content + ' '.repeat(Math.max(0, padding - 1)) + chalk.cyan('║');
|
|
995
|
+
};
|
|
996
|
+
|
|
997
|
+
console.clear();
|
|
998
|
+
displayBanner();
|
|
999
|
+
drawBoxHeaderContinue(`CONNECT ${config.name}`, boxWidth);
|
|
1000
|
+
|
|
1001
|
+
console.log(makeLine(chalk.yellow('AUTOMATIC SETUP')));
|
|
1002
|
+
console.log(makeLine(''));
|
|
1003
|
+
console.log(makeLine(chalk.white('PREPARING CONNECTION...')));
|
|
1004
|
+
|
|
1005
|
+
drawBoxFooter(boxWidth);
|
|
1006
|
+
|
|
1007
|
+
// Ensure proxy is running (will install if needed)
|
|
1008
|
+
const spinner = ora({ text: 'Setting up AI connection...', color: 'cyan' }).start();
|
|
1009
|
+
|
|
1010
|
+
try {
|
|
1011
|
+
await proxyManager.ensureRunning((msg) => {
|
|
1012
|
+
spinner.text = msg;
|
|
1013
|
+
});
|
|
1014
|
+
} catch (error) {
|
|
1015
|
+
spinner.fail(`Setup failed: ${error.message}`);
|
|
1016
|
+
await prompts.waitForEnter();
|
|
1017
|
+
return await selectProviderOption(provider);
|
|
796
1018
|
}
|
|
797
1019
|
|
|
798
|
-
|
|
1020
|
+
// Get OAuth URL from proxy
|
|
1021
|
+
spinner.text = 'Generating authorization link...';
|
|
1022
|
+
|
|
1023
|
+
let authData;
|
|
1024
|
+
try {
|
|
1025
|
+
authData = await proxyManager.getAuthUrl(provider.id);
|
|
1026
|
+
} catch (error) {
|
|
1027
|
+
spinner.fail(`Failed to get auth URL: ${error.message}`);
|
|
1028
|
+
await prompts.waitForEnter();
|
|
1029
|
+
return await selectProviderOption(provider);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
spinner.stop();
|
|
1033
|
+
|
|
1034
|
+
// Show URL to user
|
|
1035
|
+
console.clear();
|
|
1036
|
+
displayBanner();
|
|
1037
|
+
drawBoxHeaderContinue(`CONNECT ${config.name}`, boxWidth);
|
|
1038
|
+
|
|
1039
|
+
console.log(makeLine(chalk.yellow('LOGIN TO YOUR ACCOUNT')));
|
|
1040
|
+
console.log(makeLine(''));
|
|
1041
|
+
console.log(makeLine(chalk.white('1. OPEN THE LINK BELOW IN YOUR BROWSER')));
|
|
1042
|
+
console.log(makeLine(chalk.white(`2. LOGIN WITH YOUR ${config.accountName.toUpperCase()} ACCOUNT`)));
|
|
1043
|
+
console.log(makeLine(chalk.white('3. AUTHORIZE THE APPLICATION')));
|
|
1044
|
+
console.log(makeLine(''));
|
|
1045
|
+
console.log(makeLine(chalk.green('THE CONNECTION WILL BE AUTOMATIC!')));
|
|
1046
|
+
console.log(makeLine(chalk.white('NO CODE TO COPY - JUST LOGIN AND AUTHORIZE')));
|
|
1047
|
+
console.log(makeLine(''));
|
|
1048
|
+
console.log(makeLine(chalk.white('PRESS ENTER AFTER YOU AUTHORIZED...')));
|
|
1049
|
+
|
|
1050
|
+
drawBoxFooter(boxWidth);
|
|
1051
|
+
|
|
1052
|
+
// Display URL outside the box for easy copy
|
|
1053
|
+
console.log();
|
|
1054
|
+
console.log(chalk.yellow(' OPEN THIS URL IN YOUR BROWSER:'));
|
|
1055
|
+
console.log();
|
|
1056
|
+
console.log(chalk.cyan(` ${authData.url}`));
|
|
1057
|
+
console.log();
|
|
1058
|
+
|
|
1059
|
+
// Try to open browser automatically
|
|
1060
|
+
const browserOpened = await openBrowser(authData.url);
|
|
1061
|
+
if (browserOpened) {
|
|
1062
|
+
console.log(chalk.green(' Browser opened automatically!'));
|
|
1063
|
+
console.log();
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// Wait for user to press enter
|
|
1067
|
+
await prompts.waitForEnter();
|
|
1068
|
+
|
|
1069
|
+
// Check if auth was successful
|
|
1070
|
+
const authSpinner = ora({ text: 'Checking authorization...', color: 'cyan' }).start();
|
|
1071
|
+
|
|
1072
|
+
try {
|
|
1073
|
+
// Poll for auth status (with timeout)
|
|
1074
|
+
const startTime = Date.now();
|
|
1075
|
+
const timeout = 60000; // 1 minute
|
|
1076
|
+
|
|
1077
|
+
while (Date.now() - startTime < timeout) {
|
|
1078
|
+
const status = await proxyManager.pollAuthStatus(authData.state);
|
|
1079
|
+
|
|
1080
|
+
if (status.status === 'ok') {
|
|
1081
|
+
authSpinner.succeed('Authorization successful!');
|
|
1082
|
+
break;
|
|
1083
|
+
} else if (status.status === 'error') {
|
|
1084
|
+
authSpinner.fail(`Authorization failed: ${status.error}`);
|
|
1085
|
+
await prompts.waitForEnter();
|
|
1086
|
+
return await selectProviderOption(provider);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// Still waiting
|
|
1090
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
1091
|
+
}
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
// If polling fails, try to get models anyway (user might have authorized)
|
|
1094
|
+
authSpinner.text = 'Verifying connection...';
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// Fetch available models from proxy
|
|
1098
|
+
authSpinner.text = 'Fetching available models...';
|
|
1099
|
+
|
|
1100
|
+
let models = [];
|
|
1101
|
+
try {
|
|
1102
|
+
models = await proxyManager.getModels();
|
|
1103
|
+
} catch (e) {
|
|
1104
|
+
// Try again
|
|
1105
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
1106
|
+
models = await proxyManager.getModels();
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
if (!models || models.length === 0) {
|
|
1110
|
+
authSpinner.fail('No models available - authorization may have failed');
|
|
1111
|
+
console.log(chalk.red('\n Please try again and make sure to complete the login.'));
|
|
1112
|
+
await prompts.waitForEnter();
|
|
1113
|
+
return await selectProviderOption(provider);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// Filter models for this provider
|
|
1117
|
+
const providerModels = models.filter(m => {
|
|
1118
|
+
const modelLower = m.toLowerCase();
|
|
1119
|
+
if (provider.id === 'anthropic') return modelLower.includes('claude');
|
|
1120
|
+
if (provider.id === 'openai') return modelLower.includes('gpt') || modelLower.includes('o1') || modelLower.includes('o3');
|
|
1121
|
+
if (provider.id === 'gemini') return modelLower.includes('gemini');
|
|
1122
|
+
if (provider.id === 'qwen') return modelLower.includes('qwen');
|
|
1123
|
+
if (provider.id === 'iflow') return true; // iFlow has various models
|
|
1124
|
+
return true;
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
const finalModels = providerModels.length > 0 ? providerModels : models;
|
|
1128
|
+
|
|
1129
|
+
authSpinner.succeed(`Found ${finalModels.length} models`);
|
|
1130
|
+
|
|
1131
|
+
// Let user select model
|
|
1132
|
+
const selectedModel = await selectModelFromList(finalModels, config.name);
|
|
1133
|
+
if (!selectedModel) {
|
|
1134
|
+
return await selectProviderOption(provider);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// Save agent configuration (using proxy)
|
|
1138
|
+
const credentials = {
|
|
1139
|
+
useProxy: true,
|
|
1140
|
+
proxyPort: proxyManager.PROXY_PORT
|
|
1141
|
+
};
|
|
1142
|
+
|
|
1143
|
+
try {
|
|
1144
|
+
await aiService.addAgent(provider.id, config.optionId, credentials, selectedModel, config.agentName);
|
|
1145
|
+
|
|
1146
|
+
console.log(chalk.green(`\n CONNECTED TO ${config.name}`));
|
|
1147
|
+
console.log(chalk.white(` MODEL: ${selectedModel}`));
|
|
1148
|
+
console.log(chalk.white(' UNLIMITED USAGE WITH YOUR SUBSCRIPTION'));
|
|
1149
|
+
} catch (error) {
|
|
1150
|
+
console.log(chalk.red(`\n FAILED TO SAVE: ${error.message}`));
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
await prompts.waitForEnter();
|
|
1154
|
+
return await aiAgentMenu();
|
|
799
1155
|
};
|
|
800
1156
|
|
|
801
1157
|
/**
|
|
802
1158
|
* Setup OAuth with browser redirect flow (Claude, OpenAI, Gemini, iFlow)
|
|
1159
|
+
* Legacy method - kept for API key users
|
|
803
1160
|
*/
|
|
804
1161
|
const setupBrowserOAuth = async (provider, config) => {
|
|
805
1162
|
const boxWidth = getLogoWidth();
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* STRICT RULE: No mock responses. Real API calls only.
|
|
5
5
|
* If API fails → return null, not fake data.
|
|
6
|
+
*
|
|
7
|
+
* Supports two modes:
|
|
8
|
+
* 1. Direct API calls (with API keys)
|
|
9
|
+
* 2. Proxy mode (via CLIProxyAPI for subscription accounts)
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
12
|
const https = require('https');
|
|
@@ -222,6 +226,45 @@ const callGemini = async (agent, prompt, systemPrompt) => {
|
|
|
222
226
|
}
|
|
223
227
|
};
|
|
224
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Call AI via local CLIProxyAPI proxy
|
|
231
|
+
* Used for subscription accounts (ChatGPT Plus, Claude Pro, etc.)
|
|
232
|
+
* @param {Object} agent - Agent configuration
|
|
233
|
+
* @param {string} prompt - User prompt
|
|
234
|
+
* @param {string} systemPrompt - System prompt
|
|
235
|
+
* @returns {Promise<string|null>} Response text or null on error
|
|
236
|
+
*/
|
|
237
|
+
const callViaProxy = async (agent, prompt, systemPrompt) => {
|
|
238
|
+
const proxyPort = agent.credentials?.proxyPort || 8317;
|
|
239
|
+
const model = agent.model;
|
|
240
|
+
|
|
241
|
+
if (!model) return null;
|
|
242
|
+
|
|
243
|
+
const url = `http://127.0.0.1:${proxyPort}/v1/chat/completions`;
|
|
244
|
+
|
|
245
|
+
const headers = {
|
|
246
|
+
'Content-Type': 'application/json',
|
|
247
|
+
'Authorization': 'Bearer hqx-local-key'
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const body = {
|
|
251
|
+
model,
|
|
252
|
+
messages: [
|
|
253
|
+
{ role: 'system', content: systemPrompt },
|
|
254
|
+
{ role: 'user', content: prompt }
|
|
255
|
+
],
|
|
256
|
+
temperature: 0.3,
|
|
257
|
+
max_tokens: 500
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const response = await makeRequest(url, { headers, body, timeout: 30000 });
|
|
262
|
+
return response.choices?.[0]?.message?.content || null;
|
|
263
|
+
} catch (error) {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
225
268
|
/**
|
|
226
269
|
* Call AI provider based on agent configuration
|
|
227
270
|
* @param {Object} agent - Agent with providerId and credentials
|
|
@@ -232,6 +275,11 @@ const callGemini = async (agent, prompt, systemPrompt) => {
|
|
|
232
275
|
const callAI = async (agent, prompt, systemPrompt = '') => {
|
|
233
276
|
if (!agent || !agent.providerId) return null;
|
|
234
277
|
|
|
278
|
+
// Check if using proxy mode (subscription accounts)
|
|
279
|
+
if (agent.credentials?.useProxy) {
|
|
280
|
+
return callViaProxy(agent, prompt, systemPrompt);
|
|
281
|
+
}
|
|
282
|
+
|
|
235
283
|
switch (agent.providerId) {
|
|
236
284
|
case 'anthropic':
|
|
237
285
|
return callAnthropic(agent, prompt, systemPrompt);
|
|
@@ -718,8 +766,31 @@ const fetchModelsWithOAuth = async (providerId, accessToken) => {
|
|
|
718
766
|
}
|
|
719
767
|
};
|
|
720
768
|
|
|
769
|
+
/**
|
|
770
|
+
* Fetch models via local CLIProxyAPI proxy
|
|
771
|
+
* @returns {Promise<Array|null>} Array of model IDs or null
|
|
772
|
+
*/
|
|
773
|
+
const fetchModelsViaProxy = async (proxyPort = 8317) => {
|
|
774
|
+
const url = `http://127.0.0.1:${proxyPort}/v1/models`;
|
|
775
|
+
|
|
776
|
+
const headers = {
|
|
777
|
+
'Authorization': 'Bearer hqx-local-key'
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
try {
|
|
781
|
+
const response = await makeRequest(url, { method: 'GET', headers, timeout: 10000 });
|
|
782
|
+
if (response.data && Array.isArray(response.data)) {
|
|
783
|
+
return response.data.map(m => m.id || m).filter(Boolean);
|
|
784
|
+
}
|
|
785
|
+
return null;
|
|
786
|
+
} catch (error) {
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
|
|
721
791
|
module.exports = {
|
|
722
792
|
callAI,
|
|
793
|
+
callViaProxy,
|
|
723
794
|
analyzeTrading,
|
|
724
795
|
analyzePerformance,
|
|
725
796
|
getMarketAdvice,
|
|
@@ -731,5 +802,6 @@ module.exports = {
|
|
|
731
802
|
fetchGeminiModels,
|
|
732
803
|
fetchOpenAIModels,
|
|
733
804
|
fetchModelsWithOAuth,
|
|
805
|
+
fetchModelsViaProxy,
|
|
734
806
|
getValidOAuthToken
|
|
735
807
|
};
|
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLIProxyAPI Manager
|
|
3
|
+
*
|
|
4
|
+
* Automatically downloads, installs, and manages CLIProxyAPI
|
|
5
|
+
* so users can connect their AI subscription accounts (ChatGPT Plus, Claude Pro, etc.)
|
|
6
|
+
* without needing to understand technical details.
|
|
7
|
+
*
|
|
8
|
+
* Flow:
|
|
9
|
+
* 1. User selects "Connect Account" in CLI
|
|
10
|
+
* 2. We ensure CLIProxyAPI is installed and running
|
|
11
|
+
* 3. Generate OAuth URL → User opens in browser → Logs in
|
|
12
|
+
* 4. Callback to localhost → Token saved → Models available
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const https = require('https');
|
|
16
|
+
const http = require('http');
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const os = require('os');
|
|
20
|
+
const { spawn, exec } = require('child_process');
|
|
21
|
+
|
|
22
|
+
// Configuration
|
|
23
|
+
const PROXY_VERSION = 'v6.6.84';
|
|
24
|
+
const PROXY_PORT = 8317;
|
|
25
|
+
const PROXY_DIR = path.join(os.homedir(), '.hqx', 'proxy');
|
|
26
|
+
const PROXY_BIN = path.join(PROXY_DIR, process.platform === 'win32' ? 'cli-proxy-api.exe' : 'cli-proxy-api');
|
|
27
|
+
const PROXY_CONFIG = path.join(PROXY_DIR, 'config.yaml');
|
|
28
|
+
const PROXY_AUTH_DIR = path.join(PROXY_DIR, 'auths');
|
|
29
|
+
const API_KEY = 'hqx-local-key';
|
|
30
|
+
|
|
31
|
+
// GitHub release URLs
|
|
32
|
+
const getDownloadUrl = () => {
|
|
33
|
+
const platform = process.platform;
|
|
34
|
+
const arch = process.arch;
|
|
35
|
+
|
|
36
|
+
let osName, archName, ext;
|
|
37
|
+
|
|
38
|
+
if (platform === 'darwin') {
|
|
39
|
+
osName = 'darwin';
|
|
40
|
+
archName = arch === 'arm64' ? 'arm64' : 'amd64';
|
|
41
|
+
ext = 'tar.gz';
|
|
42
|
+
} else if (platform === 'win32') {
|
|
43
|
+
osName = 'windows';
|
|
44
|
+
archName = arch === 'arm64' ? 'arm64' : 'amd64';
|
|
45
|
+
ext = 'zip';
|
|
46
|
+
} else {
|
|
47
|
+
osName = 'linux';
|
|
48
|
+
archName = arch === 'arm64' ? 'arm64' : 'amd64';
|
|
49
|
+
ext = 'tar.gz';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const version = PROXY_VERSION.replace('v', '');
|
|
53
|
+
return `https://github.com/router-for-me/CLIProxyAPI/releases/download/${PROXY_VERSION}/CLIProxyAPI_${version}_${osName}_${archName}.${ext}`;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if CLIProxyAPI binary exists
|
|
58
|
+
*/
|
|
59
|
+
const isInstalled = () => {
|
|
60
|
+
return fs.existsSync(PROXY_BIN);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Check if CLIProxyAPI is running
|
|
65
|
+
*/
|
|
66
|
+
const isRunning = async () => {
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
const req = http.request({
|
|
69
|
+
hostname: '127.0.0.1',
|
|
70
|
+
port: PROXY_PORT,
|
|
71
|
+
path: '/v1/models',
|
|
72
|
+
method: 'GET',
|
|
73
|
+
headers: {
|
|
74
|
+
'Authorization': `Bearer ${API_KEY}`
|
|
75
|
+
},
|
|
76
|
+
timeout: 2000
|
|
77
|
+
}, (res) => {
|
|
78
|
+
resolve(res.statusCode === 200);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
req.on('error', () => resolve(false));
|
|
82
|
+
req.on('timeout', () => {
|
|
83
|
+
req.destroy();
|
|
84
|
+
resolve(false);
|
|
85
|
+
});
|
|
86
|
+
req.end();
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Download file from URL
|
|
92
|
+
*/
|
|
93
|
+
const downloadFile = (url, dest) => {
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
const file = fs.createWriteStream(dest);
|
|
96
|
+
|
|
97
|
+
const request = (url) => {
|
|
98
|
+
https.get(url, (res) => {
|
|
99
|
+
if (res.statusCode === 302 || res.statusCode === 301) {
|
|
100
|
+
// Follow redirect
|
|
101
|
+
request(res.headers.location);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (res.statusCode !== 200) {
|
|
106
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
res.pipe(file);
|
|
111
|
+
file.on('finish', () => {
|
|
112
|
+
file.close();
|
|
113
|
+
resolve();
|
|
114
|
+
});
|
|
115
|
+
}).on('error', (err) => {
|
|
116
|
+
fs.unlink(dest, () => {});
|
|
117
|
+
reject(err);
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
request(url);
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Extract tar.gz archive
|
|
127
|
+
*/
|
|
128
|
+
const extractTarGz = (archive, dest) => {
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
exec(`tar -xzf "${archive}" -C "${dest}"`, (err) => {
|
|
131
|
+
if (err) reject(err);
|
|
132
|
+
else resolve();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Extract zip archive (Windows)
|
|
139
|
+
*/
|
|
140
|
+
const extractZip = (archive, dest) => {
|
|
141
|
+
return new Promise((resolve, reject) => {
|
|
142
|
+
exec(`powershell -command "Expand-Archive -Path '${archive}' -DestinationPath '${dest}' -Force"`, (err) => {
|
|
143
|
+
if (err) reject(err);
|
|
144
|
+
else resolve();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Download and install CLIProxyAPI
|
|
151
|
+
* @param {Function} onProgress - Progress callback (message)
|
|
152
|
+
*/
|
|
153
|
+
const install = async (onProgress = () => {}) => {
|
|
154
|
+
// Create directories
|
|
155
|
+
if (!fs.existsSync(PROXY_DIR)) {
|
|
156
|
+
fs.mkdirSync(PROXY_DIR, { recursive: true });
|
|
157
|
+
}
|
|
158
|
+
if (!fs.existsSync(PROXY_AUTH_DIR)) {
|
|
159
|
+
fs.mkdirSync(PROXY_AUTH_DIR, { recursive: true });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
onProgress('Downloading CLIProxyAPI...');
|
|
163
|
+
|
|
164
|
+
const downloadUrl = getDownloadUrl();
|
|
165
|
+
const ext = process.platform === 'win32' ? 'zip' : 'tar.gz';
|
|
166
|
+
const archivePath = path.join(PROXY_DIR, `cliproxyapi.${ext}`);
|
|
167
|
+
|
|
168
|
+
// Download
|
|
169
|
+
await downloadFile(downloadUrl, archivePath);
|
|
170
|
+
|
|
171
|
+
onProgress('Extracting...');
|
|
172
|
+
|
|
173
|
+
// Extract
|
|
174
|
+
if (ext === 'zip') {
|
|
175
|
+
await extractZip(archivePath, PROXY_DIR);
|
|
176
|
+
} else {
|
|
177
|
+
await extractTarGz(archivePath, PROXY_DIR);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Find the binary (it might be in a subdirectory)
|
|
181
|
+
const possibleBins = [
|
|
182
|
+
path.join(PROXY_DIR, 'cli-proxy-api'),
|
|
183
|
+
path.join(PROXY_DIR, 'cli-proxy-api.exe'),
|
|
184
|
+
path.join(PROXY_DIR, 'CLIProxyAPI', 'cli-proxy-api'),
|
|
185
|
+
path.join(PROXY_DIR, 'CLIProxyAPI', 'cli-proxy-api.exe')
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
for (const bin of possibleBins) {
|
|
189
|
+
if (fs.existsSync(bin) && bin !== PROXY_BIN) {
|
|
190
|
+
fs.renameSync(bin, PROXY_BIN);
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Make executable (Unix)
|
|
196
|
+
if (process.platform !== 'win32') {
|
|
197
|
+
fs.chmodSync(PROXY_BIN, 0o755);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Create config file
|
|
201
|
+
const config = `port: ${PROXY_PORT}
|
|
202
|
+
auth-dir: "${PROXY_AUTH_DIR}"
|
|
203
|
+
api-keys:
|
|
204
|
+
- "${API_KEY}"
|
|
205
|
+
request-retry: 3
|
|
206
|
+
quota-exceeded:
|
|
207
|
+
switch-project: true
|
|
208
|
+
switch-preview-model: true
|
|
209
|
+
`;
|
|
210
|
+
|
|
211
|
+
fs.writeFileSync(PROXY_CONFIG, config);
|
|
212
|
+
|
|
213
|
+
// Cleanup archive
|
|
214
|
+
fs.unlinkSync(archivePath);
|
|
215
|
+
|
|
216
|
+
onProgress('Installation complete');
|
|
217
|
+
|
|
218
|
+
return true;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Start CLIProxyAPI in background
|
|
223
|
+
*/
|
|
224
|
+
const start = async () => {
|
|
225
|
+
if (await isRunning()) {
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!isInstalled()) {
|
|
230
|
+
throw new Error('CLIProxyAPI not installed');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return new Promise((resolve, reject) => {
|
|
234
|
+
const proc = spawn(PROXY_BIN, ['--config', PROXY_CONFIG], {
|
|
235
|
+
detached: true,
|
|
236
|
+
stdio: 'ignore',
|
|
237
|
+
cwd: PROXY_DIR
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
proc.unref();
|
|
241
|
+
|
|
242
|
+
// Wait for it to start
|
|
243
|
+
let attempts = 0;
|
|
244
|
+
const checkInterval = setInterval(async () => {
|
|
245
|
+
attempts++;
|
|
246
|
+
if (await isRunning()) {
|
|
247
|
+
clearInterval(checkInterval);
|
|
248
|
+
resolve(true);
|
|
249
|
+
} else if (attempts > 30) {
|
|
250
|
+
clearInterval(checkInterval);
|
|
251
|
+
reject(new Error('Failed to start CLIProxyAPI'));
|
|
252
|
+
}
|
|
253
|
+
}, 500);
|
|
254
|
+
});
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Stop CLIProxyAPI
|
|
259
|
+
*/
|
|
260
|
+
const stop = async () => {
|
|
261
|
+
return new Promise((resolve) => {
|
|
262
|
+
if (process.platform === 'win32') {
|
|
263
|
+
exec('taskkill /F /IM cli-proxy-api.exe', () => resolve());
|
|
264
|
+
} else {
|
|
265
|
+
exec('pkill -f cli-proxy-api', () => resolve());
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Ensure CLIProxyAPI is installed and running
|
|
272
|
+
* @param {Function} onProgress - Progress callback
|
|
273
|
+
*/
|
|
274
|
+
const ensureRunning = async (onProgress = () => {}) => {
|
|
275
|
+
if (await isRunning()) {
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (!isInstalled()) {
|
|
280
|
+
onProgress('Installing AI proxy (one-time setup)...');
|
|
281
|
+
await install(onProgress);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
onProgress('Starting AI proxy...');
|
|
285
|
+
await start();
|
|
286
|
+
|
|
287
|
+
return true;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Make request to local proxy
|
|
292
|
+
*/
|
|
293
|
+
const proxyRequest = (method, endpoint, body = null) => {
|
|
294
|
+
return new Promise((resolve, reject) => {
|
|
295
|
+
const options = {
|
|
296
|
+
hostname: '127.0.0.1',
|
|
297
|
+
port: PROXY_PORT,
|
|
298
|
+
path: endpoint,
|
|
299
|
+
method,
|
|
300
|
+
headers: {
|
|
301
|
+
'Authorization': `Bearer ${API_KEY}`,
|
|
302
|
+
'Content-Type': 'application/json'
|
|
303
|
+
},
|
|
304
|
+
timeout: 30000
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const req = http.request(options, (res) => {
|
|
308
|
+
let data = '';
|
|
309
|
+
res.on('data', chunk => data += chunk);
|
|
310
|
+
res.on('end', () => {
|
|
311
|
+
try {
|
|
312
|
+
resolve(JSON.parse(data));
|
|
313
|
+
} catch (e) {
|
|
314
|
+
resolve(data);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
req.on('error', reject);
|
|
320
|
+
req.on('timeout', () => {
|
|
321
|
+
req.destroy();
|
|
322
|
+
reject(new Error('Request timeout'));
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
if (body) {
|
|
326
|
+
req.write(JSON.stringify(body));
|
|
327
|
+
}
|
|
328
|
+
req.end();
|
|
329
|
+
});
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Get OAuth authorization URL for a provider
|
|
334
|
+
* @param {string} provider - Provider ID (anthropic, openai, gemini, qwen, iflow)
|
|
335
|
+
* @returns {Promise<{url: string, state: string}>}
|
|
336
|
+
*/
|
|
337
|
+
const getAuthUrl = async (provider) => {
|
|
338
|
+
await ensureRunning();
|
|
339
|
+
|
|
340
|
+
const endpoints = {
|
|
341
|
+
anthropic: '/v0/management/anthropic-auth-url',
|
|
342
|
+
openai: '/v0/management/codex-auth-url',
|
|
343
|
+
gemini: '/v0/management/gemini-cli-auth-url',
|
|
344
|
+
qwen: '/v0/management/qwen-auth-url',
|
|
345
|
+
iflow: '/v0/management/iflow-auth-url'
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const endpoint = endpoints[provider];
|
|
349
|
+
if (!endpoint) {
|
|
350
|
+
throw new Error(`Unknown provider: ${provider}`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const response = await proxyRequest('GET', endpoint);
|
|
354
|
+
|
|
355
|
+
if (response.status !== 'ok') {
|
|
356
|
+
throw new Error(response.error || 'Failed to get auth URL');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
url: response.url,
|
|
361
|
+
state: response.state
|
|
362
|
+
};
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Poll for OAuth authentication status
|
|
367
|
+
* @param {string} state - OAuth state from getAuthUrl
|
|
368
|
+
* @returns {Promise<{status: string, error?: string}>}
|
|
369
|
+
*/
|
|
370
|
+
const pollAuthStatus = async (state) => {
|
|
371
|
+
const response = await proxyRequest('GET', `/v0/management/get-auth-status?state=${state}`);
|
|
372
|
+
return response;
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Wait for OAuth authentication to complete
|
|
377
|
+
* @param {string} state - OAuth state
|
|
378
|
+
* @param {number} timeoutMs - Timeout in milliseconds
|
|
379
|
+
* @param {Function} onStatus - Status callback
|
|
380
|
+
* @returns {Promise<boolean>}
|
|
381
|
+
*/
|
|
382
|
+
const waitForAuth = async (state, timeoutMs = 300000, onStatus = () => {}) => {
|
|
383
|
+
const startTime = Date.now();
|
|
384
|
+
|
|
385
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
386
|
+
const status = await pollAuthStatus(state);
|
|
387
|
+
|
|
388
|
+
if (status.status === 'ok') {
|
|
389
|
+
return true;
|
|
390
|
+
} else if (status.status === 'error') {
|
|
391
|
+
throw new Error(status.error || 'Authentication failed');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
onStatus('Waiting for authorization...');
|
|
395
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
throw new Error('Authentication timeout');
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Get available models from the proxy
|
|
403
|
+
* @returns {Promise<Array<string>>}
|
|
404
|
+
*/
|
|
405
|
+
const getModels = async () => {
|
|
406
|
+
await ensureRunning();
|
|
407
|
+
|
|
408
|
+
const response = await proxyRequest('GET', '/v1/models');
|
|
409
|
+
|
|
410
|
+
if (response.data && Array.isArray(response.data)) {
|
|
411
|
+
return response.data.map(m => m.id || m).filter(Boolean);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return [];
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Get list of authenticated accounts
|
|
419
|
+
* @returns {Promise<Array>}
|
|
420
|
+
*/
|
|
421
|
+
const getAuthFiles = async () => {
|
|
422
|
+
await ensureRunning();
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
const response = await proxyRequest('GET', '/v0/management/auth-files');
|
|
426
|
+
return response.files || [];
|
|
427
|
+
} catch (e) {
|
|
428
|
+
return [];
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Make chat completion request through proxy
|
|
434
|
+
* @param {string} model - Model ID
|
|
435
|
+
* @param {Array} messages - Chat messages
|
|
436
|
+
* @param {Object} options - Additional options
|
|
437
|
+
* @returns {Promise<Object>}
|
|
438
|
+
*/
|
|
439
|
+
const chatCompletion = async (model, messages, options = {}) => {
|
|
440
|
+
await ensureRunning();
|
|
441
|
+
|
|
442
|
+
const body = {
|
|
443
|
+
model,
|
|
444
|
+
messages,
|
|
445
|
+
...options
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
return proxyRequest('POST', '/v1/chat/completions', body);
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Check if user has any connected accounts
|
|
453
|
+
*/
|
|
454
|
+
const hasConnectedAccounts = async () => {
|
|
455
|
+
try {
|
|
456
|
+
const files = await getAuthFiles();
|
|
457
|
+
return files.length > 0;
|
|
458
|
+
} catch (e) {
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Get provider name from auth file
|
|
465
|
+
*/
|
|
466
|
+
const getProviderFromAuthFile = (filename) => {
|
|
467
|
+
if (filename.includes('claude') || filename.includes('anthropic')) return 'anthropic';
|
|
468
|
+
if (filename.includes('openai') || filename.includes('codex')) return 'openai';
|
|
469
|
+
if (filename.includes('gemini') || filename.includes('google')) return 'gemini';
|
|
470
|
+
if (filename.includes('qwen')) return 'qwen';
|
|
471
|
+
if (filename.includes('iflow')) return 'iflow';
|
|
472
|
+
return 'unknown';
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
// ============================================
|
|
476
|
+
// REMOTE OAUTH (for VPS/Server users)
|
|
477
|
+
// Uses cli.hedgequantx.com as OAuth relay
|
|
478
|
+
// ============================================
|
|
479
|
+
|
|
480
|
+
const REMOTE_OAUTH_URL = 'https://cli.hedgequantx.com';
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Make HTTPS request
|
|
484
|
+
*/
|
|
485
|
+
const httpsRequest = (url, options, body = null) => {
|
|
486
|
+
return new Promise((resolve, reject) => {
|
|
487
|
+
const parsedUrl = new URL(url);
|
|
488
|
+
const req = https.request({
|
|
489
|
+
hostname: parsedUrl.hostname,
|
|
490
|
+
port: 443,
|
|
491
|
+
path: parsedUrl.pathname + parsedUrl.search,
|
|
492
|
+
method: options.method || 'GET',
|
|
493
|
+
headers: options.headers || {}
|
|
494
|
+
}, (res) => {
|
|
495
|
+
let data = '';
|
|
496
|
+
res.on('data', chunk => data += chunk);
|
|
497
|
+
res.on('end', () => {
|
|
498
|
+
try {
|
|
499
|
+
resolve(JSON.parse(data));
|
|
500
|
+
} catch (e) {
|
|
501
|
+
resolve(data);
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
req.on('error', reject);
|
|
507
|
+
req.on('timeout', () => {
|
|
508
|
+
req.destroy();
|
|
509
|
+
reject(new Error('Request timeout'));
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
if (body) {
|
|
513
|
+
req.write(typeof body === 'string' ? body : JSON.stringify(body));
|
|
514
|
+
}
|
|
515
|
+
req.end();
|
|
516
|
+
});
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Create Remote OAuth session
|
|
521
|
+
* @param {string} provider - Provider ID (anthropic, openai, gemini)
|
|
522
|
+
* @returns {Promise<{sessionId: string, authUrl: string}>}
|
|
523
|
+
*/
|
|
524
|
+
const createRemoteSession = async (provider) => {
|
|
525
|
+
const response = await httpsRequest(
|
|
526
|
+
`${REMOTE_OAUTH_URL}/oauth/session/create`,
|
|
527
|
+
{
|
|
528
|
+
method: 'POST',
|
|
529
|
+
headers: { 'Content-Type': 'application/json' }
|
|
530
|
+
},
|
|
531
|
+
JSON.stringify({ provider })
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
if (response.error) {
|
|
535
|
+
throw new Error(response.error);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return {
|
|
539
|
+
sessionId: response.sessionId,
|
|
540
|
+
authUrl: response.authUrl
|
|
541
|
+
};
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Poll Remote OAuth session status
|
|
546
|
+
* @param {string} sessionId - Session ID from createRemoteSession
|
|
547
|
+
* @returns {Promise<{status: string, error?: string}>}
|
|
548
|
+
*/
|
|
549
|
+
const pollRemoteSession = async (sessionId) => {
|
|
550
|
+
const response = await httpsRequest(
|
|
551
|
+
`${REMOTE_OAUTH_URL}/oauth/session/${sessionId}/status`,
|
|
552
|
+
{ method: 'GET' }
|
|
553
|
+
);
|
|
554
|
+
return response;
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Get tokens from Remote OAuth session
|
|
559
|
+
* @param {string} sessionId - Session ID
|
|
560
|
+
* @returns {Promise<{provider: string, tokens: Object}>}
|
|
561
|
+
*/
|
|
562
|
+
const getRemoteTokens = async (sessionId) => {
|
|
563
|
+
const response = await httpsRequest(
|
|
564
|
+
`${REMOTE_OAUTH_URL}/oauth/session/${sessionId}/tokens`,
|
|
565
|
+
{ method: 'GET' }
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
if (response.error) {
|
|
569
|
+
throw new Error(response.error);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return response;
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Wait for Remote OAuth to complete
|
|
577
|
+
* @param {string} sessionId - Session ID
|
|
578
|
+
* @param {number} timeoutMs - Timeout in milliseconds
|
|
579
|
+
* @param {Function} onStatus - Status callback
|
|
580
|
+
* @returns {Promise<{provider: string, tokens: Object}>}
|
|
581
|
+
*/
|
|
582
|
+
const waitForRemoteAuth = async (sessionId, timeoutMs = 300000, onStatus = () => {}) => {
|
|
583
|
+
const startTime = Date.now();
|
|
584
|
+
|
|
585
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
586
|
+
const status = await pollRemoteSession(sessionId);
|
|
587
|
+
|
|
588
|
+
if (status.status === 'success') {
|
|
589
|
+
return await getRemoteTokens(sessionId);
|
|
590
|
+
} else if (status.status === 'error') {
|
|
591
|
+
throw new Error(status.error || 'Authentication failed');
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
onStatus('Waiting for authorization...');
|
|
595
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
throw new Error('Authentication timeout');
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Detect if we're running on a server (no display/browser)
|
|
603
|
+
* @returns {boolean}
|
|
604
|
+
*/
|
|
605
|
+
const isServerEnvironment = () => {
|
|
606
|
+
// Check for common server indicators
|
|
607
|
+
if (process.env.SSH_CONNECTION || process.env.SSH_CLIENT) return true;
|
|
608
|
+
if (process.env.DISPLAY === undefined && process.platform === 'linux') return true;
|
|
609
|
+
if (process.env.TERM === 'dumb') return true;
|
|
610
|
+
if (process.env.HQX_REMOTE_OAUTH === '1') return true; // Force remote mode
|
|
611
|
+
return false;
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Detect if browser can be opened
|
|
616
|
+
* @returns {boolean}
|
|
617
|
+
*/
|
|
618
|
+
const canOpenBrowser = () => {
|
|
619
|
+
// On macOS/Windows, we can usually open browser
|
|
620
|
+
if (process.platform === 'darwin' || process.platform === 'win32') return true;
|
|
621
|
+
// On Linux, check for display
|
|
622
|
+
if (process.env.DISPLAY) return true;
|
|
623
|
+
return false;
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
module.exports = {
|
|
627
|
+
// Local OAuth (CLIProxyAPI)
|
|
628
|
+
isInstalled,
|
|
629
|
+
isRunning,
|
|
630
|
+
install,
|
|
631
|
+
start,
|
|
632
|
+
stop,
|
|
633
|
+
ensureRunning,
|
|
634
|
+
getAuthUrl,
|
|
635
|
+
pollAuthStatus,
|
|
636
|
+
waitForAuth,
|
|
637
|
+
getModels,
|
|
638
|
+
getAuthFiles,
|
|
639
|
+
chatCompletion,
|
|
640
|
+
hasConnectedAccounts,
|
|
641
|
+
getProviderFromAuthFile,
|
|
642
|
+
PROXY_PORT,
|
|
643
|
+
PROXY_DIR,
|
|
644
|
+
API_KEY,
|
|
645
|
+
|
|
646
|
+
// Remote OAuth (cli.hedgequantx.com)
|
|
647
|
+
createRemoteSession,
|
|
648
|
+
pollRemoteSession,
|
|
649
|
+
getRemoteTokens,
|
|
650
|
+
waitForRemoteAuth,
|
|
651
|
+
isServerEnvironment,
|
|
652
|
+
canOpenBrowser,
|
|
653
|
+
REMOTE_OAUTH_URL
|
|
654
|
+
};
|