hedgequantx 2.5.23 → 2.5.24

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.5.23",
3
+ "version": "2.5.24",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -11,6 +11,7 @@ const { prompts } = require('../utils');
11
11
  const aiService = require('../services/ai');
12
12
  const { getCategories, getProvidersByCategory } = require('../services/ai/providers');
13
13
  const tokenScanner = require('../services/ai/token-scanner');
14
+ const oauthAnthropic = require('../services/ai/oauth-anthropic');
14
15
 
15
16
  /**
16
17
  * Main AI Agent menu
@@ -845,10 +846,135 @@ const getCredentialInstructions = (provider, option, field) => {
845
846
  return instructions[field] || { title: field.toUpperCase(), steps: [] };
846
847
  };
847
848
 
849
+ /**
850
+ * Setup OAuth connection for Anthropic Claude Pro/Max
851
+ */
852
+ const setupOAuthConnection = async (provider) => {
853
+ const boxWidth = getLogoWidth();
854
+ const W = boxWidth - 2;
855
+
856
+ const makeLine = (content) => {
857
+ const plainLen = content.replace(/\x1b\[[0-9;]*m/g, '').length;
858
+ const padding = W - plainLen;
859
+ return chalk.cyan('║') + ' ' + content + ' '.repeat(Math.max(0, padding - 1)) + chalk.cyan('║');
860
+ };
861
+
862
+ console.clear();
863
+ displayBanner();
864
+ drawBoxHeaderContinue('CLAUDE PRO/MAX LOGIN', boxWidth);
865
+
866
+ console.log(makeLine(chalk.yellow('OAUTH AUTHENTICATION')));
867
+ console.log(makeLine(''));
868
+ console.log(makeLine(chalk.white('1. A BROWSER WINDOW WILL OPEN')));
869
+ console.log(makeLine(chalk.white('2. LOGIN WITH YOUR CLAUDE ACCOUNT')));
870
+ console.log(makeLine(chalk.white('3. COPY THE AUTHORIZATION CODE')));
871
+ console.log(makeLine(chalk.white('4. PASTE IT HERE')));
872
+ console.log(makeLine(''));
873
+ console.log(makeLine(chalk.gray('OPENING BROWSER IN 3 SECONDS...')));
874
+
875
+ drawBoxFooter(boxWidth);
876
+
877
+ // Generate OAuth URL
878
+ const { url, verifier } = oauthAnthropic.authorize('max');
879
+
880
+ // Wait a moment then open browser
881
+ await new Promise(resolve => setTimeout(resolve, 3000));
882
+ openBrowser(url);
883
+
884
+ // Redraw with code input
885
+ console.clear();
886
+ displayBanner();
887
+ drawBoxHeaderContinue('CLAUDE PRO/MAX LOGIN', boxWidth);
888
+
889
+ console.log(makeLine(chalk.green('BROWSER OPENED')));
890
+ console.log(makeLine(''));
891
+ console.log(makeLine(chalk.white('AFTER LOGGING IN, YOU WILL SEE A CODE')));
892
+ console.log(makeLine(chalk.white('COPY THE ENTIRE CODE AND PASTE IT BELOW')));
893
+ console.log(makeLine(''));
894
+ console.log(makeLine(chalk.gray('THE CODE LOOKS LIKE: abc123...#xyz789...')));
895
+ console.log(makeLine(''));
896
+ console.log(makeLine(chalk.gray('TYPE < TO CANCEL')));
897
+
898
+ drawBoxFooter(boxWidth);
899
+ console.log();
900
+
901
+ const code = await prompts.textInput(chalk.cyan('PASTE AUTHORIZATION CODE:'));
902
+
903
+ if (!code || code === '<') {
904
+ return await selectProviderOption(provider);
905
+ }
906
+
907
+ // Exchange code for tokens
908
+ const spinner = ora({ text: 'EXCHANGING CODE FOR TOKENS...', color: 'cyan' }).start();
909
+
910
+ const result = await oauthAnthropic.exchange(code.trim(), verifier);
911
+
912
+ if (result.type === 'failed') {
913
+ spinner.fail(`AUTHENTICATION FAILED: ${result.error || 'Invalid code'}`);
914
+ await prompts.waitForEnter();
915
+ return await selectProviderOption(provider);
916
+ }
917
+
918
+ spinner.text = 'FETCHING AVAILABLE MODELS...';
919
+
920
+ // Store OAuth credentials
921
+ const credentials = {
922
+ oauth: {
923
+ access: result.access,
924
+ refresh: result.refresh,
925
+ expires: result.expires
926
+ }
927
+ };
928
+
929
+ // Fetch models using OAuth token
930
+ const { fetchAnthropicModelsOAuth } = require('../services/ai/client');
931
+ const models = await fetchAnthropicModelsOAuth(result.access);
932
+
933
+ if (!models || models.length === 0) {
934
+ // Use default models if API doesn't return list
935
+ spinner.warn('COULD NOT FETCH MODEL LIST, USING DEFAULTS');
936
+ } else {
937
+ spinner.succeed(`FOUND ${models.length} MODELS`);
938
+ }
939
+
940
+ // Let user select model
941
+ const availableModels = models && models.length > 0 ? models : [
942
+ 'claude-sonnet-4-20250514',
943
+ 'claude-sonnet-4-5-20250514',
944
+ 'claude-3-5-sonnet-20241022',
945
+ 'claude-3-5-haiku-20241022',
946
+ 'claude-3-opus-20240229'
947
+ ];
948
+
949
+ const selectedModel = await selectModelFromList(availableModels, 'CLAUDE PRO/MAX');
950
+ if (!selectedModel) {
951
+ return await selectProviderOption(provider);
952
+ }
953
+
954
+ // Add agent with OAuth credentials
955
+ try {
956
+ await aiService.addAgent('anthropic', 'oauth_max', credentials, selectedModel, 'Claude Pro/Max');
957
+
958
+ console.log(chalk.green('\n CONNECTED TO CLAUDE PRO/MAX'));
959
+ console.log(chalk.gray(` MODEL: ${selectedModel}`));
960
+ console.log(chalk.gray(' UNLIMITED USAGE WITH YOUR SUBSCRIPTION'));
961
+ } catch (error) {
962
+ console.log(chalk.red(`\n FAILED TO SAVE: ${error.message}`));
963
+ }
964
+
965
+ await prompts.waitForEnter();
966
+ return await aiAgentMenu();
967
+ };
968
+
848
969
  /**
849
970
  * Setup connection with credentials
850
971
  */
851
972
  const setupConnection = async (provider, option) => {
973
+ // Handle OAuth flow separately
974
+ if (option.authType === 'oauth') {
975
+ return await setupOAuthConnection(provider);
976
+ }
977
+
852
978
  const boxWidth = getLogoWidth();
853
979
  const W = boxWidth - 2;
854
980
 
@@ -105,8 +105,32 @@ const callOpenAICompatible = async (agent, prompt, systemPrompt) => {
105
105
  }
106
106
  };
107
107
 
108
+ /**
109
+ * Get valid OAuth token (refresh if needed)
110
+ * @param {Object} credentials - Agent credentials with oauth data
111
+ * @returns {Promise<string|null>} Valid access token or null
112
+ */
113
+ const getValidOAuthToken = async (credentials) => {
114
+ if (!credentials?.oauth) return null;
115
+
116
+ const oauthAnthropic = require('./oauth-anthropic');
117
+ const validToken = await oauthAnthropic.getValidToken(credentials.oauth);
118
+
119
+ if (!validToken) return null;
120
+
121
+ // If token was refreshed, we should update storage (handled by caller)
122
+ if (validToken.refreshed) {
123
+ credentials.oauth.access = validToken.access;
124
+ credentials.oauth.refresh = validToken.refresh;
125
+ credentials.oauth.expires = validToken.expires;
126
+ }
127
+
128
+ return validToken.access;
129
+ };
130
+
108
131
  /**
109
132
  * Call Anthropic Claude API
133
+ * Supports both API key and OAuth authentication
110
134
  * @param {Object} agent - Agent configuration
111
135
  * @param {string} prompt - User prompt
112
136
  * @param {string} systemPrompt - System prompt
@@ -116,19 +140,31 @@ const callAnthropic = async (agent, prompt, systemPrompt) => {
116
140
  const provider = getProvider('anthropic');
117
141
  if (!provider) return null;
118
142
 
119
- const apiKey = agent.credentials?.apiKey;
120
143
  const model = agent.model || provider.defaultModel;
121
-
122
- if (!apiKey) return null;
123
-
124
144
  const url = `${provider.endpoint}/messages`;
125
145
 
126
- const headers = {
146
+ // Determine authentication method
147
+ const isOAuth = agent.credentials?.oauth?.refresh;
148
+ let headers = {
127
149
  'Content-Type': 'application/json',
128
- 'x-api-key': apiKey,
129
150
  'anthropic-version': '2023-06-01'
130
151
  };
131
152
 
153
+ if (isOAuth) {
154
+ // OAuth Bearer token authentication
155
+ const accessToken = await getValidOAuthToken(agent.credentials);
156
+ if (!accessToken) return null;
157
+
158
+ headers['Authorization'] = `Bearer ${accessToken}`;
159
+ headers['anthropic-beta'] = 'oauth-2025-04-20,interleaved-thinking-2025-05-14';
160
+ } else {
161
+ // Standard API key authentication
162
+ const apiKey = agent.credentials?.apiKey;
163
+ if (!apiKey) return null;
164
+
165
+ headers['x-api-key'] = apiKey;
166
+ }
167
+
132
168
  const body = {
133
169
  model,
134
170
  max_tokens: 500,
@@ -273,7 +309,7 @@ Analyze and provide recommendation.`;
273
309
  };
274
310
 
275
311
  /**
276
- * Fetch available models from Anthropic API
312
+ * Fetch available models from Anthropic API (API Key auth)
277
313
  * @param {string} apiKey - API key
278
314
  * @returns {Promise<Array|null>} Array of model IDs or null on error
279
315
  *
@@ -300,6 +336,36 @@ const fetchAnthropicModels = async (apiKey) => {
300
336
  }
301
337
  };
302
338
 
339
+ /**
340
+ * Fetch available models from Anthropic API (OAuth auth)
341
+ * @param {string} accessToken - OAuth access token
342
+ * @returns {Promise<Array|null>} Array of model IDs or null on error
343
+ *
344
+ * Data source: https://api.anthropic.com/v1/models (GET with Bearer token)
345
+ */
346
+ const fetchAnthropicModelsOAuth = async (accessToken) => {
347
+ if (!accessToken) return null;
348
+
349
+ const url = 'https://api.anthropic.com/v1/models';
350
+
351
+ const headers = {
352
+ 'Authorization': `Bearer ${accessToken}`,
353
+ 'anthropic-version': '2023-06-01',
354
+ 'anthropic-beta': 'oauth-2025-04-20'
355
+ };
356
+
357
+ try {
358
+ const response = await makeRequest(url, { method: 'GET', headers, timeout: 10000 });
359
+ if (response.data && Array.isArray(response.data)) {
360
+ return response.data.map(m => m.id).filter(Boolean);
361
+ }
362
+ return null;
363
+ } catch (error) {
364
+ // OAuth may not support /models endpoint, return null to use defaults
365
+ return null;
366
+ }
367
+ };
368
+
303
369
  /**
304
370
  * Fetch available models from OpenAI-compatible API
305
371
  * @param {string} endpoint - API endpoint
@@ -339,5 +405,7 @@ module.exports = {
339
405
  callAnthropic,
340
406
  callGemini,
341
407
  fetchAnthropicModels,
342
- fetchOpenAIModels
408
+ fetchAnthropicModelsOAuth,
409
+ fetchOpenAIModels,
410
+ getValidOAuthToken
343
411
  };
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Anthropic OAuth Authentication
3
+ *
4
+ * Implements OAuth 2.0 with PKCE for Anthropic Claude Pro/Max plans.
5
+ * Based on the public OAuth flow used by OpenCode.
6
+ *
7
+ * Data source: Anthropic OAuth API (https://console.anthropic.com/v1/oauth/token)
8
+ */
9
+
10
+ const crypto = require('crypto');
11
+ const https = require('https');
12
+
13
+ // Public OAuth Client ID (same as OpenCode - registered with Anthropic)
14
+ const CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
15
+ const REDIRECT_URI = 'https://console.anthropic.com/oauth/code/callback';
16
+
17
+ /**
18
+ * Generate PKCE code verifier and challenge
19
+ * @returns {Object} { verifier: string, challenge: string }
20
+ */
21
+ const generatePKCE = () => {
22
+ // Generate a random 32-byte code verifier (base64url encoded)
23
+ const verifier = crypto.randomBytes(32)
24
+ .toString('base64')
25
+ .replace(/\+/g, '-')
26
+ .replace(/\//g, '_')
27
+ .replace(/=/g, '');
28
+
29
+ // Generate SHA256 hash of verifier, then base64url encode it
30
+ const challenge = crypto.createHash('sha256')
31
+ .update(verifier)
32
+ .digest('base64')
33
+ .replace(/\+/g, '-')
34
+ .replace(/\//g, '_')
35
+ .replace(/=/g, '');
36
+
37
+ return { verifier, challenge };
38
+ };
39
+
40
+ /**
41
+ * Generate OAuth authorization URL
42
+ * @param {'max' | 'console'} mode - 'max' for Claude Pro/Max, 'console' for API key creation
43
+ * @returns {Object} { url: string, verifier: string }
44
+ */
45
+ const authorize = (mode = 'max') => {
46
+ const pkce = generatePKCE();
47
+
48
+ const baseUrl = mode === 'max'
49
+ ? 'https://claude.ai/oauth/authorize'
50
+ : 'https://console.anthropic.com/oauth/authorize';
51
+
52
+ const url = new URL(baseUrl);
53
+ url.searchParams.set('code', 'true');
54
+ url.searchParams.set('client_id', CLIENT_ID);
55
+ url.searchParams.set('response_type', 'code');
56
+ url.searchParams.set('redirect_uri', REDIRECT_URI);
57
+ url.searchParams.set('scope', 'org:create_api_key user:profile user:inference');
58
+ url.searchParams.set('code_challenge', pkce.challenge);
59
+ url.searchParams.set('code_challenge_method', 'S256');
60
+ url.searchParams.set('state', pkce.verifier);
61
+
62
+ return {
63
+ url: url.toString(),
64
+ verifier: pkce.verifier
65
+ };
66
+ };
67
+
68
+ /**
69
+ * Make HTTPS request
70
+ * @param {string} url - Full URL
71
+ * @param {Object} options - Request options
72
+ * @returns {Promise<Object>} Response JSON
73
+ */
74
+ const makeRequest = (url, options) => {
75
+ return new Promise((resolve, reject) => {
76
+ const req = https.request(url, {
77
+ method: options.method || 'POST',
78
+ headers: options.headers || {}
79
+ }, (res) => {
80
+ let data = '';
81
+ res.on('data', chunk => data += chunk);
82
+ res.on('end', () => {
83
+ try {
84
+ const json = JSON.parse(data);
85
+ if (res.statusCode >= 200 && res.statusCode < 300) {
86
+ resolve(json);
87
+ } else {
88
+ reject(new Error(json.error?.message || `HTTP ${res.statusCode}: ${data}`));
89
+ }
90
+ } catch (e) {
91
+ reject(new Error(`Invalid JSON response: ${data.substring(0, 200)}`));
92
+ }
93
+ });
94
+ });
95
+
96
+ req.on('error', reject);
97
+
98
+ if (options.body) {
99
+ req.write(JSON.stringify(options.body));
100
+ }
101
+ req.end();
102
+ });
103
+ };
104
+
105
+ /**
106
+ * Exchange authorization code for tokens
107
+ * @param {string} code - Authorization code from callback (format: code#state)
108
+ * @param {string} verifier - PKCE code verifier
109
+ * @returns {Promise<Object>} { type: 'success', access: string, refresh: string, expires: number } or { type: 'failed' }
110
+ *
111
+ * Data source: https://console.anthropic.com/v1/oauth/token (POST)
112
+ */
113
+ const exchange = async (code, verifier) => {
114
+ try {
115
+ // Code format from callback: "authorization_code#state"
116
+ const splits = code.split('#');
117
+ const authCode = splits[0];
118
+ const state = splits[1] || '';
119
+
120
+ const response = await makeRequest('https://console.anthropic.com/v1/oauth/token', {
121
+ method: 'POST',
122
+ headers: {
123
+ 'Content-Type': 'application/json'
124
+ },
125
+ body: {
126
+ code: authCode,
127
+ state: state,
128
+ grant_type: 'authorization_code',
129
+ client_id: CLIENT_ID,
130
+ redirect_uri: REDIRECT_URI,
131
+ code_verifier: verifier
132
+ }
133
+ });
134
+
135
+ return {
136
+ type: 'success',
137
+ access: response.access_token,
138
+ refresh: response.refresh_token,
139
+ expires: Date.now() + (response.expires_in * 1000)
140
+ };
141
+ } catch (error) {
142
+ return {
143
+ type: 'failed',
144
+ error: error.message
145
+ };
146
+ }
147
+ };
148
+
149
+ /**
150
+ * Refresh access token using refresh token
151
+ * @param {string} refreshToken - The refresh token
152
+ * @returns {Promise<Object>} { type: 'success', access: string, refresh: string, expires: number } or { type: 'failed' }
153
+ *
154
+ * Data source: https://console.anthropic.com/v1/oauth/token (POST)
155
+ */
156
+ const refreshToken = async (refreshToken) => {
157
+ try {
158
+ const response = await makeRequest('https://console.anthropic.com/v1/oauth/token', {
159
+ method: 'POST',
160
+ headers: {
161
+ 'Content-Type': 'application/json'
162
+ },
163
+ body: {
164
+ grant_type: 'refresh_token',
165
+ refresh_token: refreshToken,
166
+ client_id: CLIENT_ID
167
+ }
168
+ });
169
+
170
+ return {
171
+ type: 'success',
172
+ access: response.access_token,
173
+ refresh: response.refresh_token,
174
+ expires: Date.now() + (response.expires_in * 1000)
175
+ };
176
+ } catch (error) {
177
+ return {
178
+ type: 'failed',
179
+ error: error.message
180
+ };
181
+ }
182
+ };
183
+
184
+ /**
185
+ * Create an API key using OAuth token (for console mode)
186
+ * @param {string} accessToken - The access token
187
+ * @returns {Promise<Object>} { type: 'success', key: string } or { type: 'failed' }
188
+ *
189
+ * Data source: https://api.anthropic.com/api/oauth/claude_cli/create_api_key (POST)
190
+ */
191
+ const createApiKey = async (accessToken) => {
192
+ try {
193
+ const response = await makeRequest('https://api.anthropic.com/api/oauth/claude_cli/create_api_key', {
194
+ method: 'POST',
195
+ headers: {
196
+ 'Content-Type': 'application/json',
197
+ 'Authorization': `Bearer ${accessToken}`
198
+ }
199
+ });
200
+
201
+ return {
202
+ type: 'success',
203
+ key: response.raw_key
204
+ };
205
+ } catch (error) {
206
+ return {
207
+ type: 'failed',
208
+ error: error.message
209
+ };
210
+ }
211
+ };
212
+
213
+ /**
214
+ * Get valid access token (refresh if expired)
215
+ * @param {Object} oauthData - OAuth data { access, refresh, expires }
216
+ * @returns {Promise<Object>} { access: string, refresh: string, expires: number, refreshed: boolean }
217
+ */
218
+ const getValidToken = async (oauthData) => {
219
+ if (!oauthData || !oauthData.refresh) {
220
+ return null;
221
+ }
222
+
223
+ // Check if token is expired or will expire in the next 5 minutes
224
+ const expirationBuffer = 5 * 60 * 1000; // 5 minutes
225
+ if (oauthData.expires && oauthData.expires > Date.now() + expirationBuffer) {
226
+ return {
227
+ ...oauthData,
228
+ refreshed: false
229
+ };
230
+ }
231
+
232
+ // Token expired or about to expire, refresh it
233
+ const result = await refreshToken(oauthData.refresh);
234
+ if (result.type === 'success') {
235
+ return {
236
+ access: result.access,
237
+ refresh: result.refresh,
238
+ expires: result.expires,
239
+ refreshed: true
240
+ };
241
+ }
242
+
243
+ return null;
244
+ };
245
+
246
+ /**
247
+ * Check if credentials are OAuth tokens
248
+ * @param {Object} credentials - Agent credentials
249
+ * @returns {boolean}
250
+ */
251
+ const isOAuthCredentials = (credentials) => {
252
+ return credentials && credentials.oauth && credentials.oauth.refresh;
253
+ };
254
+
255
+ module.exports = {
256
+ CLIENT_ID,
257
+ REDIRECT_URI,
258
+ generatePKCE,
259
+ authorize,
260
+ exchange,
261
+ refreshToken,
262
+ createApiKey,
263
+ getValidToken,
264
+ isOAuthCredentials
265
+ };
@@ -46,6 +46,16 @@ const PROVIDERS = {
46
46
  models: [], // Fetched from API at runtime
47
47
  defaultModel: null, // Will use first model from API
48
48
  options: [
49
+ {
50
+ id: 'oauth_max',
51
+ label: 'CLAUDE PRO/MAX (OAUTH)',
52
+ description: [
53
+ 'Login with your Claude subscription',
54
+ 'Unlimited usage with your plan'
55
+ ],
56
+ fields: ['oauth'],
57
+ authType: 'oauth'
58
+ },
49
59
  {
50
60
  id: 'api_key',
51
61
  label: 'API KEY (PAY-PER-USE)',