hedgequantx 2.5.22 → 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.22",
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
@@ -494,14 +495,39 @@ const showExistingTokens = async () => {
494
495
  return await showExistingTokens();
495
496
  }
496
497
 
497
- // Add as new agent
498
- const model = provider.defaultModel;
498
+ spinner.text = 'FETCHING AVAILABLE MODELS...';
499
+
500
+ // Fetch models from API with the token
501
+ const { fetchAnthropicModels, fetchOpenAIModels } = require('../services/ai/client');
502
+
503
+ let models = null;
504
+ if (selectedToken.provider === 'anthropic') {
505
+ models = await fetchAnthropicModels(credentials.apiKey);
506
+ } else {
507
+ models = await fetchOpenAIModels(provider.endpoint, credentials.apiKey);
508
+ }
509
+
510
+ if (!models || models.length === 0) {
511
+ spinner.fail('COULD NOT FETCH MODELS FROM API');
512
+ await prompts.waitForEnter();
513
+ return await showExistingTokens();
514
+ }
515
+
516
+ spinner.succeed(`FOUND ${models.length} MODELS`);
517
+
518
+ // Let user select model
519
+ const selectedModel = await selectModelFromList(models, provider.name);
520
+ if (!selectedModel) {
521
+ return await showExistingTokens();
522
+ }
523
+
524
+ // Add agent with selected model
499
525
  const agentName = `${provider.name} (${selectedToken.source})`;
500
- await aiService.addAgent(selectedToken.provider, 'api_key', credentials, model, agentName);
526
+ await aiService.addAgent(selectedToken.provider, 'api_key', credentials, selectedModel, agentName);
501
527
 
502
- spinner.succeed(`AGENT ADDED: ${provider.name}`);
528
+ console.log(chalk.green(`\n AGENT ADDED: ${provider.name}`));
503
529
  console.log(chalk.gray(` SOURCE: ${selectedToken.source}`));
504
- console.log(chalk.gray(` MODEL: ${model}`));
530
+ console.log(chalk.gray(` MODEL: ${selectedModel}`));
505
531
 
506
532
  await prompts.waitForEnter();
507
533
  return await aiAgentMenu();
@@ -820,10 +846,135 @@ const getCredentialInstructions = (provider, option, field) => {
820
846
  return instructions[field] || { title: field.toUpperCase(), steps: [] };
821
847
  };
822
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
+
823
969
  /**
824
970
  * Setup connection with credentials
825
971
  */
826
972
  const setupConnection = async (provider, option) => {
973
+ // Handle OAuth flow separately
974
+ if (option.authType === 'oauth') {
975
+ return await setupOAuthConnection(provider);
976
+ }
977
+
827
978
  const boxWidth = getLogoWidth();
828
979
  const W = boxWidth - 2;
829
980
 
@@ -942,8 +1093,67 @@ const setupConnection = async (provider, option) => {
942
1093
  return await aiAgentMenu();
943
1094
  };
944
1095
 
1096
+ /**
1097
+ * Select model from a list (used when adding new agent)
1098
+ * @param {Array} models - Array of model IDs from API
1099
+ * @param {string} providerName - Provider name for display
1100
+ * @returns {string|null} Selected model ID or null if cancelled
1101
+ *
1102
+ * Data source: models array comes from provider API (/v1/models)
1103
+ */
1104
+ const selectModelFromList = async (models, providerName) => {
1105
+ const boxWidth = getLogoWidth();
1106
+ const W = boxWidth - 2;
1107
+
1108
+ const makeLine = (content) => {
1109
+ const plainLen = content.replace(/\x1b\[[0-9;]*m/g, '').length;
1110
+ const padding = W - plainLen;
1111
+ return chalk.cyan('║') + ' ' + content + ' '.repeat(Math.max(0, padding - 1)) + chalk.cyan('║');
1112
+ };
1113
+
1114
+ console.clear();
1115
+ displayBanner();
1116
+ drawBoxHeaderContinue(`SELECT MODEL - ${providerName}`, boxWidth);
1117
+
1118
+ if (!models || models.length === 0) {
1119
+ console.log(makeLine(chalk.red('NO MODELS AVAILABLE')));
1120
+ console.log(makeLine(chalk.gray('[<] BACK')));
1121
+ drawBoxFooter(boxWidth);
1122
+ await prompts.waitForEnter();
1123
+ return null;
1124
+ }
1125
+
1126
+ // Sort models (newest first)
1127
+ const sortedModels = [...models].sort((a, b) => b.localeCompare(a));
1128
+
1129
+ // Display models from API
1130
+ sortedModels.forEach((model, index) => {
1131
+ const displayModel = model.length > W - 10 ? model.substring(0, W - 13) + '...' : model;
1132
+ console.log(makeLine(chalk.cyan(`[${index + 1}] ${displayModel}`)));
1133
+ });
1134
+
1135
+ console.log(makeLine(''));
1136
+ console.log(makeLine(chalk.gray('[<] BACK')));
1137
+
1138
+ drawBoxFooter(boxWidth);
1139
+
1140
+ const choice = await prompts.textInput(chalk.cyan('SELECT MODEL:'));
1141
+
1142
+ if (choice === '<' || choice?.toLowerCase() === 'b') {
1143
+ return null;
1144
+ }
1145
+
1146
+ const index = parseInt(choice) - 1;
1147
+ if (isNaN(index) || index < 0 || index >= sortedModels.length) {
1148
+ return await selectModelFromList(models, providerName);
1149
+ }
1150
+
1151
+ return sortedModels[index];
1152
+ };
1153
+
945
1154
  /**
946
1155
  * Select/change model for an agent
1156
+ * Fetches available models from the provider's API
947
1157
  */
948
1158
  const selectModel = async (agent) => {
949
1159
  const boxWidth = getLogoWidth();
@@ -959,26 +1169,44 @@ const selectModel = async (agent) => {
959
1169
  displayBanner();
960
1170
  drawBoxHeaderContinue(`SELECT MODEL - ${agent.name}`, boxWidth);
961
1171
 
962
- const models = agent.provider?.models || [];
1172
+ console.log(makeLine(chalk.gray('FETCHING AVAILABLE MODELS FROM API...')));
1173
+ drawBoxFooter(boxWidth);
1174
+
1175
+ // Fetch models from real API
1176
+ const { fetchAnthropicModels, fetchOpenAIModels } = require('../services/ai/client');
1177
+
1178
+ let models = null;
1179
+ const agentCredentials = aiService.getAgentCredentials(agent.id);
1180
+
1181
+ if (agent.providerId === 'anthropic') {
1182
+ models = await fetchAnthropicModels(agentCredentials?.apiKey);
1183
+ } else {
1184
+ // OpenAI-compatible providers
1185
+ const endpoint = agentCredentials?.endpoint || agent.provider?.endpoint;
1186
+ models = await fetchOpenAIModels(endpoint, agentCredentials?.apiKey);
1187
+ }
963
1188
 
964
- if (models.length === 0) {
965
- console.log(makeLine(chalk.gray('NO PREDEFINED MODELS. ENTER MODEL NAME MANUALLY.')));
1189
+ // Redraw with results
1190
+ console.clear();
1191
+ displayBanner();
1192
+ drawBoxHeaderContinue(`SELECT MODEL - ${agent.name}`, boxWidth);
1193
+
1194
+ if (!models || models.length === 0) {
1195
+ console.log(makeLine(chalk.red('COULD NOT FETCH MODELS FROM API')));
1196
+ console.log(makeLine(chalk.gray('Check your API key or network connection.')));
966
1197
  console.log(makeLine(''));
967
1198
  console.log(makeLine(chalk.gray('[<] BACK')));
968
1199
  drawBoxFooter(boxWidth);
969
1200
 
970
- const model = await prompts.textInput('ENTER MODEL NAME (OR < TO GO BACK):');
971
- if (!model || model === '<') {
972
- return await aiAgentMenu();
973
- }
974
- aiService.updateAgent(agent.id, { model });
975
- console.log(chalk.green(`\n MODEL CHANGED TO: ${model}`));
976
1201
  await prompts.waitForEnter();
977
1202
  return await aiAgentMenu();
978
1203
  }
979
1204
 
1205
+ // Sort models (newest first typically)
1206
+ models.sort((a, b) => b.localeCompare(a));
1207
+
1208
+ // Display models from API
980
1209
  models.forEach((model, index) => {
981
- // Truncate long model names
982
1210
  const displayModel = model.length > W - 10 ? model.substring(0, W - 13) + '...' : model;
983
1211
  const currentMarker = model === agent.model ? chalk.yellow(' (CURRENT)') : '';
984
1212
  console.log(makeLine(chalk.cyan(`[${index + 1}] ${displayModel}`) + currentMarker));
@@ -995,22 +1223,6 @@ const selectModel = async (agent) => {
995
1223
  return await aiAgentMenu();
996
1224
  }
997
1225
 
998
- // Custom model option
999
- if (choice?.toLowerCase() === 'c') {
1000
- console.log(chalk.gray('\n Enter any model name supported by your provider.'));
1001
- console.log(chalk.gray(' Examples: claude-opus-4-20250514, gpt-4o-2024-11-20, etc.\n'));
1002
-
1003
- const customModel = await prompts.textInput(chalk.cyan('ENTER MODEL NAME:'));
1004
- if (!customModel || customModel === '<') {
1005
- return await selectModel(agent);
1006
- }
1007
-
1008
- aiService.updateAgent(agent.id, { model: customModel.trim() });
1009
- console.log(chalk.green(`\n MODEL CHANGED TO: ${customModel.trim()}`));
1010
- await prompts.waitForEnter();
1011
- return await aiAgentMenu();
1012
- }
1013
-
1014
1226
  const index = parseInt(choice) - 1;
1015
1227
  if (isNaN(index) || index < 0 || index >= models.length) {
1016
1228
  return await selectModel(agent);
@@ -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,
@@ -272,10 +308,104 @@ Analyze and provide recommendation.`;
272
308
  }
273
309
  };
274
310
 
311
+ /**
312
+ * Fetch available models from Anthropic API (API Key auth)
313
+ * @param {string} apiKey - API key
314
+ * @returns {Promise<Array|null>} Array of model IDs or null on error
315
+ *
316
+ * Data source: https://api.anthropic.com/v1/models (GET)
317
+ */
318
+ const fetchAnthropicModels = async (apiKey) => {
319
+ if (!apiKey) return null;
320
+
321
+ const url = 'https://api.anthropic.com/v1/models';
322
+
323
+ const headers = {
324
+ 'x-api-key': apiKey,
325
+ 'anthropic-version': '2023-06-01'
326
+ };
327
+
328
+ try {
329
+ const response = await makeRequest(url, { method: 'GET', headers, timeout: 10000 });
330
+ if (response.data && Array.isArray(response.data)) {
331
+ return response.data.map(m => m.id).filter(Boolean);
332
+ }
333
+ return null;
334
+ } catch (error) {
335
+ return null;
336
+ }
337
+ };
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
+
369
+ /**
370
+ * Fetch available models from OpenAI-compatible API
371
+ * @param {string} endpoint - API endpoint
372
+ * @param {string} apiKey - API key
373
+ * @returns {Promise<Array|null>} Array of model IDs or null on error
374
+ *
375
+ * Data source: {endpoint}/models (GET)
376
+ */
377
+ const fetchOpenAIModels = async (endpoint, apiKey) => {
378
+ if (!endpoint) return null;
379
+
380
+ const url = `${endpoint}/models`;
381
+
382
+ const headers = {
383
+ 'Content-Type': 'application/json'
384
+ };
385
+
386
+ if (apiKey) {
387
+ headers['Authorization'] = `Bearer ${apiKey}`;
388
+ }
389
+
390
+ try {
391
+ const response = await makeRequest(url, { method: 'GET', headers, timeout: 10000 });
392
+ if (response.data && Array.isArray(response.data)) {
393
+ return response.data.map(m => m.id).filter(Boolean);
394
+ }
395
+ return null;
396
+ } catch (error) {
397
+ return null;
398
+ }
399
+ };
400
+
275
401
  module.exports = {
276
402
  callAI,
277
403
  analyzeTrading,
278
404
  callOpenAICompatible,
279
405
  callAnthropic,
280
- callGemini
406
+ callGemini,
407
+ fetchAnthropicModels,
408
+ fetchAnthropicModelsOAuth,
409
+ fetchOpenAIModels,
410
+ getValidOAuthToken
281
411
  };
@@ -159,6 +159,20 @@ const getAgent = (agentId) => {
159
159
  };
160
160
  };
161
161
 
162
+ /**
163
+ * Get agent credentials by ID
164
+ * @param {string} agentId - Agent ID
165
+ * @returns {Object|null} Credentials object or null
166
+ */
167
+ const getAgentCredentials = (agentId) => {
168
+ const aiSettings = getAISettings();
169
+ const agents = aiSettings.agents || [];
170
+ const agent = agents.find(a => a.id === agentId);
171
+
172
+ if (!agent) return null;
173
+ return agent.credentials || null;
174
+ };
175
+
162
176
  /**
163
177
  * Set active agent
164
178
  */
@@ -607,6 +621,7 @@ module.exports = {
607
621
  getAgents,
608
622
  getAgentCount,
609
623
  getAgent,
624
+ getAgentCredentials,
610
625
  getActiveAgent,
611
626
  setActiveAgent,
612
627
  addAgent,
@@ -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
+ };
@@ -43,22 +43,19 @@ const PROVIDERS = {
43
43
  name: 'CLAUDE (ANTHROPIC)',
44
44
  description: 'Direct connection to Claude',
45
45
  category: 'direct',
46
- models: [
47
- // Claude 4.5 (Latest flagship)
48
- 'claude-opus-4-5-20250514',
49
- // Claude 4
50
- 'claude-opus-4-20250514',
51
- 'claude-sonnet-4-20250514',
52
- // Claude 3.5
53
- 'claude-3-5-sonnet-20241022',
54
- 'claude-3-5-haiku-20241022',
55
- // Claude 3
56
- 'claude-3-opus-20240229',
57
- 'claude-3-sonnet-20240229',
58
- 'claude-3-haiku-20240307'
59
- ],
60
- defaultModel: 'claude-opus-4-5-20250514',
46
+ models: [], // Fetched from API at runtime
47
+ defaultModel: null, // Will use first model from API
61
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
+ },
62
59
  {
63
60
  id: 'api_key',
64
61
  label: 'API KEY (PAY-PER-USE)',