hedgequantx 2.6.93 → 2.6.95
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 +178 -4
- package/src/services/ai/client.js +166 -7
- package/src/services/ai/proxy-manager.js +493 -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,7 @@ const getOAuthConfig = (providerId) => {
|
|
|
781
782
|
|
|
782
783
|
/**
|
|
783
784
|
* Setup OAuth connection for any provider with OAuth support
|
|
785
|
+
* Uses CLIProxyAPI for automatic token management
|
|
784
786
|
*/
|
|
785
787
|
const setupOAuthConnection = async (provider) => {
|
|
786
788
|
const config = getOAuthConfig(provider.id);
|
|
@@ -790,16 +792,188 @@ const setupOAuthConnection = async (provider) => {
|
|
|
790
792
|
return await selectProviderOption(provider);
|
|
791
793
|
}
|
|
792
794
|
|
|
793
|
-
// Use
|
|
794
|
-
|
|
795
|
-
|
|
795
|
+
// Use the new proxy-based OAuth flow (automatic, no code copying needed)
|
|
796
|
+
// This works for all providers: Claude, ChatGPT, Gemini, Qwen, iFlow
|
|
797
|
+
return await setupProxyOAuth(provider, config);
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Setup OAuth via CLIProxyAPI (automatic local proxy)
|
|
802
|
+
* This is the simplified flow for users with subscription plans
|
|
803
|
+
*/
|
|
804
|
+
const setupProxyOAuth = async (provider, config) => {
|
|
805
|
+
const boxWidth = getLogoWidth();
|
|
806
|
+
const W = boxWidth - 2;
|
|
807
|
+
|
|
808
|
+
const makeLine = (content) => {
|
|
809
|
+
const plainLen = content.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
810
|
+
const padding = W - plainLen;
|
|
811
|
+
return chalk.cyan('║') + ' ' + content + ' '.repeat(Math.max(0, padding - 1)) + chalk.cyan('║');
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
console.clear();
|
|
815
|
+
displayBanner();
|
|
816
|
+
drawBoxHeaderContinue(`CONNECT ${config.name}`, boxWidth);
|
|
817
|
+
|
|
818
|
+
console.log(makeLine(chalk.yellow('AUTOMATIC SETUP')));
|
|
819
|
+
console.log(makeLine(''));
|
|
820
|
+
console.log(makeLine(chalk.white('PREPARING CONNECTION...')));
|
|
821
|
+
|
|
822
|
+
drawBoxFooter(boxWidth);
|
|
823
|
+
|
|
824
|
+
// Ensure proxy is running (will install if needed)
|
|
825
|
+
const spinner = ora({ text: 'Setting up AI connection...', color: 'cyan' }).start();
|
|
826
|
+
|
|
827
|
+
try {
|
|
828
|
+
await proxyManager.ensureRunning((msg) => {
|
|
829
|
+
spinner.text = msg;
|
|
830
|
+
});
|
|
831
|
+
} catch (error) {
|
|
832
|
+
spinner.fail(`Setup failed: ${error.message}`);
|
|
833
|
+
await prompts.waitForEnter();
|
|
834
|
+
return await selectProviderOption(provider);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Get OAuth URL from proxy
|
|
838
|
+
spinner.text = 'Generating authorization link...';
|
|
839
|
+
|
|
840
|
+
let authData;
|
|
841
|
+
try {
|
|
842
|
+
authData = await proxyManager.getAuthUrl(provider.id);
|
|
843
|
+
} catch (error) {
|
|
844
|
+
spinner.fail(`Failed to get auth URL: ${error.message}`);
|
|
845
|
+
await prompts.waitForEnter();
|
|
846
|
+
return await selectProviderOption(provider);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
spinner.stop();
|
|
850
|
+
|
|
851
|
+
// Show URL to user
|
|
852
|
+
console.clear();
|
|
853
|
+
displayBanner();
|
|
854
|
+
drawBoxHeaderContinue(`CONNECT ${config.name}`, boxWidth);
|
|
855
|
+
|
|
856
|
+
console.log(makeLine(chalk.yellow('LOGIN TO YOUR ACCOUNT')));
|
|
857
|
+
console.log(makeLine(''));
|
|
858
|
+
console.log(makeLine(chalk.white('1. OPEN THE LINK BELOW IN YOUR BROWSER')));
|
|
859
|
+
console.log(makeLine(chalk.white(`2. LOGIN WITH YOUR ${config.accountName.toUpperCase()} ACCOUNT`)));
|
|
860
|
+
console.log(makeLine(chalk.white('3. AUTHORIZE THE APPLICATION')));
|
|
861
|
+
console.log(makeLine(''));
|
|
862
|
+
console.log(makeLine(chalk.green('THE CONNECTION WILL BE AUTOMATIC!')));
|
|
863
|
+
console.log(makeLine(chalk.white('NO CODE TO COPY - JUST LOGIN AND AUTHORIZE')));
|
|
864
|
+
console.log(makeLine(''));
|
|
865
|
+
console.log(makeLine(chalk.white('PRESS ENTER AFTER YOU AUTHORIZED...')));
|
|
866
|
+
|
|
867
|
+
drawBoxFooter(boxWidth);
|
|
868
|
+
|
|
869
|
+
// Display URL outside the box for easy copy
|
|
870
|
+
console.log();
|
|
871
|
+
console.log(chalk.yellow(' OPEN THIS URL IN YOUR BROWSER:'));
|
|
872
|
+
console.log();
|
|
873
|
+
console.log(chalk.cyan(` ${authData.url}`));
|
|
874
|
+
console.log();
|
|
875
|
+
|
|
876
|
+
// Try to open browser automatically
|
|
877
|
+
const browserOpened = await openBrowser(authData.url);
|
|
878
|
+
if (browserOpened) {
|
|
879
|
+
console.log(chalk.green(' Browser opened automatically!'));
|
|
880
|
+
console.log();
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Wait for user to press enter
|
|
884
|
+
await prompts.waitForEnter();
|
|
885
|
+
|
|
886
|
+
// Check if auth was successful
|
|
887
|
+
const authSpinner = ora({ text: 'Checking authorization...', color: 'cyan' }).start();
|
|
888
|
+
|
|
889
|
+
try {
|
|
890
|
+
// Poll for auth status (with timeout)
|
|
891
|
+
const startTime = Date.now();
|
|
892
|
+
const timeout = 60000; // 1 minute
|
|
893
|
+
|
|
894
|
+
while (Date.now() - startTime < timeout) {
|
|
895
|
+
const status = await proxyManager.pollAuthStatus(authData.state);
|
|
896
|
+
|
|
897
|
+
if (status.status === 'ok') {
|
|
898
|
+
authSpinner.succeed('Authorization successful!');
|
|
899
|
+
break;
|
|
900
|
+
} else if (status.status === 'error') {
|
|
901
|
+
authSpinner.fail(`Authorization failed: ${status.error}`);
|
|
902
|
+
await prompts.waitForEnter();
|
|
903
|
+
return await selectProviderOption(provider);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// Still waiting
|
|
907
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
908
|
+
}
|
|
909
|
+
} catch (error) {
|
|
910
|
+
// If polling fails, try to get models anyway (user might have authorized)
|
|
911
|
+
authSpinner.text = 'Verifying connection...';
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// Fetch available models from proxy
|
|
915
|
+
authSpinner.text = 'Fetching available models...';
|
|
916
|
+
|
|
917
|
+
let models = [];
|
|
918
|
+
try {
|
|
919
|
+
models = await proxyManager.getModels();
|
|
920
|
+
} catch (e) {
|
|
921
|
+
// Try again
|
|
922
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
923
|
+
models = await proxyManager.getModels();
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
if (!models || models.length === 0) {
|
|
927
|
+
authSpinner.fail('No models available - authorization may have failed');
|
|
928
|
+
console.log(chalk.red('\n Please try again and make sure to complete the login.'));
|
|
929
|
+
await prompts.waitForEnter();
|
|
930
|
+
return await selectProviderOption(provider);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// Filter models for this provider
|
|
934
|
+
const providerModels = models.filter(m => {
|
|
935
|
+
const modelLower = m.toLowerCase();
|
|
936
|
+
if (provider.id === 'anthropic') return modelLower.includes('claude');
|
|
937
|
+
if (provider.id === 'openai') return modelLower.includes('gpt') || modelLower.includes('o1') || modelLower.includes('o3');
|
|
938
|
+
if (provider.id === 'gemini') return modelLower.includes('gemini');
|
|
939
|
+
if (provider.id === 'qwen') return modelLower.includes('qwen');
|
|
940
|
+
if (provider.id === 'iflow') return true; // iFlow has various models
|
|
941
|
+
return true;
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
const finalModels = providerModels.length > 0 ? providerModels : models;
|
|
945
|
+
|
|
946
|
+
authSpinner.succeed(`Found ${finalModels.length} models`);
|
|
947
|
+
|
|
948
|
+
// Let user select model
|
|
949
|
+
const selectedModel = await selectModelFromList(finalModels, config.name);
|
|
950
|
+
if (!selectedModel) {
|
|
951
|
+
return await selectProviderOption(provider);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Save agent configuration (using proxy)
|
|
955
|
+
const credentials = {
|
|
956
|
+
useProxy: true,
|
|
957
|
+
proxyPort: proxyManager.PROXY_PORT
|
|
958
|
+
};
|
|
959
|
+
|
|
960
|
+
try {
|
|
961
|
+
await aiService.addAgent(provider.id, config.optionId, credentials, selectedModel, config.agentName);
|
|
962
|
+
|
|
963
|
+
console.log(chalk.green(`\n CONNECTED TO ${config.name}`));
|
|
964
|
+
console.log(chalk.white(` MODEL: ${selectedModel}`));
|
|
965
|
+
console.log(chalk.white(' UNLIMITED USAGE WITH YOUR SUBSCRIPTION'));
|
|
966
|
+
} catch (error) {
|
|
967
|
+
console.log(chalk.red(`\n FAILED TO SAVE: ${error.message}`));
|
|
796
968
|
}
|
|
797
969
|
|
|
798
|
-
|
|
970
|
+
await prompts.waitForEnter();
|
|
971
|
+
return await aiAgentMenu();
|
|
799
972
|
};
|
|
800
973
|
|
|
801
974
|
/**
|
|
802
975
|
* Setup OAuth with browser redirect flow (Claude, OpenAI, Gemini, iFlow)
|
|
976
|
+
* Legacy method - kept for API key users
|
|
803
977
|
*/
|
|
804
978
|
const setupBrowserOAuth = async (provider, config) => {
|
|
805
979
|
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);
|
|
@@ -592,9 +640,16 @@ const fetchOpenAIModels = async (endpoint, apiKey) => {
|
|
|
592
640
|
|
|
593
641
|
/**
|
|
594
642
|
* Fetch available models for OAuth-authenticated providers
|
|
643
|
+
* Uses multiple API endpoints to discover available models
|
|
644
|
+
*
|
|
595
645
|
* @param {string} providerId - Provider ID (anthropic, openai, gemini, etc.)
|
|
596
646
|
* @param {string} accessToken - OAuth access token
|
|
597
|
-
* @returns {Promise<Array|null>} Array of model IDs
|
|
647
|
+
* @returns {Promise<Array|null>} Array of model IDs from API, null if unavailable
|
|
648
|
+
*
|
|
649
|
+
* Data sources:
|
|
650
|
+
* - OpenAI: https://api.openai.com/v1/models (GET)
|
|
651
|
+
* - Anthropic: https://api.anthropic.com/v1/models (GET)
|
|
652
|
+
* - Gemini: https://generativelanguage.googleapis.com/v1/models (GET)
|
|
598
653
|
*/
|
|
599
654
|
const fetchModelsWithOAuth = async (providerId, accessToken) => {
|
|
600
655
|
if (!accessToken) return null;
|
|
@@ -604,17 +659,48 @@ const fetchModelsWithOAuth = async (providerId, accessToken) => {
|
|
|
604
659
|
case 'anthropic':
|
|
605
660
|
return await fetchAnthropicModelsOAuth(accessToken);
|
|
606
661
|
|
|
607
|
-
case 'openai':
|
|
608
|
-
// OpenAI
|
|
609
|
-
|
|
662
|
+
case 'openai': {
|
|
663
|
+
// Try OpenAI /v1/models endpoint with OAuth token
|
|
664
|
+
const openaiModels = await fetchOpenAIModels('https://api.openai.com/v1', accessToken);
|
|
665
|
+
if (openaiModels && openaiModels.length > 0) {
|
|
666
|
+
return openaiModels;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Try alternative: ChatGPT backend API (for Plus/Pro plans)
|
|
670
|
+
try {
|
|
671
|
+
const chatgptUrl = 'https://chatgpt.com/backend-api/models';
|
|
672
|
+
const chatgptHeaders = {
|
|
673
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
674
|
+
'Content-Type': 'application/json'
|
|
675
|
+
};
|
|
676
|
+
const chatgptResponse = await makeRequest(chatgptUrl, {
|
|
677
|
+
method: 'GET',
|
|
678
|
+
headers: chatgptHeaders,
|
|
679
|
+
timeout: 10000
|
|
680
|
+
});
|
|
681
|
+
if (chatgptResponse.models && Array.isArray(chatgptResponse.models)) {
|
|
682
|
+
return chatgptResponse.models
|
|
683
|
+
.map(m => m.slug || m.id || m.name)
|
|
684
|
+
.filter(Boolean);
|
|
685
|
+
}
|
|
686
|
+
} catch (e) {
|
|
687
|
+
// ChatGPT backend not available
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
610
692
|
|
|
611
|
-
case 'gemini':
|
|
612
|
-
// Gemini OAuth -
|
|
693
|
+
case 'gemini': {
|
|
694
|
+
// Gemini OAuth - fetch from Generative Language API
|
|
613
695
|
const geminiUrl = 'https://generativelanguage.googleapis.com/v1/models';
|
|
614
696
|
const geminiHeaders = {
|
|
615
697
|
'Authorization': `Bearer ${accessToken}`
|
|
616
698
|
};
|
|
617
|
-
const geminiResponse = await makeRequest(geminiUrl, {
|
|
699
|
+
const geminiResponse = await makeRequest(geminiUrl, {
|
|
700
|
+
method: 'GET',
|
|
701
|
+
headers: geminiHeaders,
|
|
702
|
+
timeout: 10000
|
|
703
|
+
});
|
|
618
704
|
if (geminiResponse.models && Array.isArray(geminiResponse.models)) {
|
|
619
705
|
return geminiResponse.models
|
|
620
706
|
.filter(m => m.supportedGenerationMethods?.includes('generateContent'))
|
|
@@ -622,6 +708,55 @@ const fetchModelsWithOAuth = async (providerId, accessToken) => {
|
|
|
622
708
|
.filter(Boolean);
|
|
623
709
|
}
|
|
624
710
|
return null;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
case 'qwen': {
|
|
714
|
+
// Qwen - try to fetch models from Alibaba Cloud API
|
|
715
|
+
try {
|
|
716
|
+
const qwenUrl = 'https://dashscope.aliyuncs.com/api/v1/models';
|
|
717
|
+
const qwenHeaders = {
|
|
718
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
719
|
+
'Content-Type': 'application/json'
|
|
720
|
+
};
|
|
721
|
+
const qwenResponse = await makeRequest(qwenUrl, {
|
|
722
|
+
method: 'GET',
|
|
723
|
+
headers: qwenHeaders,
|
|
724
|
+
timeout: 10000
|
|
725
|
+
});
|
|
726
|
+
if (qwenResponse.data && Array.isArray(qwenResponse.data)) {
|
|
727
|
+
return qwenResponse.data
|
|
728
|
+
.map(m => m.id || m.model_id)
|
|
729
|
+
.filter(Boolean);
|
|
730
|
+
}
|
|
731
|
+
} catch (e) {
|
|
732
|
+
// Qwen API may not support model listing
|
|
733
|
+
}
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
case 'iflow': {
|
|
738
|
+
// iFlow - fetch models from iFlow API
|
|
739
|
+
try {
|
|
740
|
+
const iflowUrl = 'https://apis.iflow.cn/v1/models';
|
|
741
|
+
const iflowHeaders = {
|
|
742
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
743
|
+
'Content-Type': 'application/json'
|
|
744
|
+
};
|
|
745
|
+
const iflowResponse = await makeRequest(iflowUrl, {
|
|
746
|
+
method: 'GET',
|
|
747
|
+
headers: iflowHeaders,
|
|
748
|
+
timeout: 10000
|
|
749
|
+
});
|
|
750
|
+
if (iflowResponse.data && Array.isArray(iflowResponse.data)) {
|
|
751
|
+
return iflowResponse.data
|
|
752
|
+
.map(m => m.id)
|
|
753
|
+
.filter(Boolean);
|
|
754
|
+
}
|
|
755
|
+
} catch (e) {
|
|
756
|
+
// iFlow API may not support model listing
|
|
757
|
+
}
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
625
760
|
|
|
626
761
|
default:
|
|
627
762
|
return null;
|
|
@@ -631,8 +766,31 @@ const fetchModelsWithOAuth = async (providerId, accessToken) => {
|
|
|
631
766
|
}
|
|
632
767
|
};
|
|
633
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
|
+
|
|
634
791
|
module.exports = {
|
|
635
792
|
callAI,
|
|
793
|
+
callViaProxy,
|
|
636
794
|
analyzeTrading,
|
|
637
795
|
analyzePerformance,
|
|
638
796
|
getMarketAdvice,
|
|
@@ -644,5 +802,6 @@ module.exports = {
|
|
|
644
802
|
fetchGeminiModels,
|
|
645
803
|
fetchOpenAIModels,
|
|
646
804
|
fetchModelsWithOAuth,
|
|
805
|
+
fetchModelsViaProxy,
|
|
647
806
|
getValidOAuthToken
|
|
648
807
|
};
|
|
@@ -0,0 +1,493 @@
|
|
|
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
|
+
module.exports = {
|
|
476
|
+
isInstalled,
|
|
477
|
+
isRunning,
|
|
478
|
+
install,
|
|
479
|
+
start,
|
|
480
|
+
stop,
|
|
481
|
+
ensureRunning,
|
|
482
|
+
getAuthUrl,
|
|
483
|
+
pollAuthStatus,
|
|
484
|
+
waitForAuth,
|
|
485
|
+
getModels,
|
|
486
|
+
getAuthFiles,
|
|
487
|
+
chatCompletion,
|
|
488
|
+
hasConnectedAccounts,
|
|
489
|
+
getProviderFromAuthFile,
|
|
490
|
+
PROXY_PORT,
|
|
491
|
+
PROXY_DIR,
|
|
492
|
+
API_KEY
|
|
493
|
+
};
|