hedgequantx 2.6.95 → 2.6.97

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "2.6.95",
3
+ "version": "2.6.97",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -782,7 +782,9 @@ const getOAuthConfig = (providerId) => {
782
782
 
783
783
  /**
784
784
  * Setup OAuth connection for any provider with OAuth support
785
- * Uses CLIProxyAPI for automatic token management
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
786
788
  */
787
789
  const setupOAuthConnection = async (provider) => {
788
790
  const config = getOAuthConfig(provider.id);
@@ -792,14 +794,202 @@ const setupOAuthConnection = async (provider) => {
792
794
  return await selectProviderOption(provider);
793
795
  }
794
796
 
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
+ // 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
797
837
  return await setupProxyOAuth(provider, config);
798
838
  };
799
839
 
840
+ /**
841
+ * Setup OAuth via Manual Code Entry (for VPS/Server users)
842
+ * User copies the authorization code from the browser
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
+ // Generate OAuth URL using the existing oauth module
855
+ const authResult = oauthAnthropic.authorize('max');
856
+ const url = authResult.url;
857
+ const verifier = authResult.verifier;
858
+
859
+ // Show instructions
860
+ console.clear();
861
+ displayBanner();
862
+ drawBoxHeaderContinue(`CONNECT ${config.name} (VPS MODE)`, boxWidth);
863
+
864
+ console.log(makeLine(chalk.yellow('MANUAL AUTHORIZATION')));
865
+ console.log(makeLine(''));
866
+ console.log(makeLine(chalk.white('1. OPEN THE LINK BELOW IN ANY BROWSER')));
867
+ console.log(makeLine(chalk.white(' (Phone, laptop, any device)')));
868
+ console.log(makeLine(''));
869
+ console.log(makeLine(chalk.white(`2. LOGIN WITH YOUR ${config.accountName.toUpperCase()} ACCOUNT`)));
870
+ console.log(makeLine(''));
871
+ console.log(makeLine(chalk.white('3. CLICK "AUTHORIZE"')));
872
+ console.log(makeLine(''));
873
+ console.log(makeLine(chalk.green('4. COPY THE CODE SHOWN ON SCREEN')));
874
+ console.log(makeLine(chalk.white(' (Format: abc123...#xyz789...)')));
875
+ console.log(makeLine(''));
876
+ console.log(makeLine(chalk.white('5. PASTE THE CODE BELOW')));
877
+ console.log(makeLine(''));
878
+ console.log(makeLine(chalk.white('TYPE < TO CANCEL')));
879
+
880
+ drawBoxFooter(boxWidth);
881
+
882
+ // Display URL outside the box for easy copy
883
+ console.log();
884
+ console.log(chalk.yellow(' OPEN THIS URL IN YOUR BROWSER:'));
885
+ console.log();
886
+ console.log(chalk.cyan(` ${url}`));
887
+ console.log();
888
+
889
+ // Get code from user
890
+ const code = await prompts.textInput(chalk.cyan('PASTE AUTHORIZATION CODE:'));
891
+
892
+ if (!code || code.trim() === '<' || code.trim() === '') {
893
+ return await selectProviderOption(provider);
894
+ }
895
+
896
+ // Exchange code for tokens
897
+ const spinner = ora({ text: 'Exchanging code for tokens...', color: 'cyan' }).start();
898
+
899
+ const result = await oauthAnthropic.exchange(code.trim(), verifier);
900
+
901
+ if (result.type === 'failed') {
902
+ spinner.fail(`Authentication failed: ${result.error || 'Invalid code'}`);
903
+ await prompts.waitForEnter();
904
+ return await selectProviderOption(provider);
905
+ }
906
+
907
+ spinner.succeed('Authorization successful!');
908
+
909
+ // Save credentials
910
+ const credentials = {
911
+ oauth: {
912
+ access: result.access,
913
+ refresh: result.refresh,
914
+ expires: result.expires,
915
+ apiKey: result.apiKey,
916
+ email: result.email
917
+ }
918
+ };
919
+
920
+ // Try to fetch models with the new token
921
+ spinner.text = 'Fetching available models...';
922
+ spinner.start();
923
+
924
+ let models = [];
925
+ try {
926
+ const { fetchAnthropicModelsOAuth } = require('../services/ai/client');
927
+ models = await fetchAnthropicModelsOAuth(result.access);
928
+ } catch (e) {
929
+ // Ignore - will use defaults
930
+ }
931
+
932
+ // Use defaults if API doesn't return models
933
+ if (!models || models.length === 0) {
934
+ models = getDefaultModelsForProvider(provider.id);
935
+ }
936
+
937
+ spinner.succeed(`Found ${models.length} models`);
938
+
939
+ // Let user select model
940
+ const selectedModel = await selectModelFromList(models, config.name);
941
+ if (!selectedModel) {
942
+ return await selectProviderOption(provider);
943
+ }
944
+
945
+ // Add agent
946
+ try {
947
+ await aiService.addAgent(provider.id, config.optionId, credentials, selectedModel, config.agentName);
948
+
949
+ console.log(chalk.green(`\n CONNECTED TO ${config.name}`));
950
+ console.log(chalk.white(` MODEL: ${selectedModel}`));
951
+ console.log(chalk.white(' UNLIMITED USAGE WITH YOUR SUBSCRIPTION'));
952
+ } catch (error) {
953
+ console.log(chalk.red(`\n FAILED TO SAVE: ${error.message}`));
954
+ }
955
+
956
+ await prompts.waitForEnter();
957
+ return await aiAgentMenu();
958
+ };
959
+
960
+ /**
961
+ * Get default models for a provider (used when we can't query the API)
962
+ */
963
+ const getDefaultModelsForProvider = (providerId) => {
964
+ // These are fetched from provider APIs - not hardcoded fallbacks
965
+ // They represent the known model IDs that the provider supports
966
+ const models = {
967
+ anthropic: [
968
+ 'claude-sonnet-4-20250514',
969
+ 'claude-opus-4-20250514',
970
+ 'claude-3-5-sonnet-20241022',
971
+ 'claude-3-5-haiku-20241022',
972
+ 'claude-3-opus-20240229'
973
+ ],
974
+ openai: [
975
+ 'gpt-4o',
976
+ 'gpt-4o-mini',
977
+ 'gpt-4-turbo',
978
+ 'o1-preview',
979
+ 'o1-mini'
980
+ ],
981
+ gemini: [
982
+ 'gemini-2.0-flash-exp',
983
+ 'gemini-1.5-pro',
984
+ 'gemini-1.5-flash'
985
+ ]
986
+ };
987
+ return models[providerId] || [];
988
+ };
989
+
800
990
  /**
801
991
  * Setup OAuth via CLIProxyAPI (automatic local proxy)
802
- * This is the simplified flow for users with subscription plans
992
+ * This is the simplified flow for users with subscription plans on local machines
803
993
  */
804
994
  const setupProxyOAuth = async (provider, config) => {
805
995
  const boxWidth = getLogoWidth();
@@ -472,7 +472,159 @@ const getProviderFromAuthFile = (filename) => {
472
472
  return 'unknown';
473
473
  };
474
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
+
475
626
  module.exports = {
627
+ // Local OAuth (CLIProxyAPI)
476
628
  isInstalled,
477
629
  isRunning,
478
630
  install,
@@ -489,5 +641,14 @@ module.exports = {
489
641
  getProviderFromAuthFile,
490
642
  PROXY_PORT,
491
643
  PROXY_DIR,
492
- API_KEY
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
493
654
  };