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 +1 -1
- package/src/menus/ai-agent.js +243 -31
- package/src/services/ai/client.js +137 -7
- package/src/services/ai/index.js +15 -0
- package/src/services/ai/oauth-anthropic.js +265 -0
- package/src/services/ai/providers/index.js +12 -15
package/package.json
CHANGED
package/src/menus/ai-agent.js
CHANGED
|
@@ -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
|
-
|
|
498
|
-
|
|
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,
|
|
526
|
+
await aiService.addAgent(selectedToken.provider, 'api_key', credentials, selectedModel, agentName);
|
|
501
527
|
|
|
502
|
-
|
|
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: ${
|
|
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
|
-
|
|
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
|
-
|
|
965
|
-
|
|
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
|
-
|
|
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
|
};
|
package/src/services/ai/index.js
CHANGED
|
@@ -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
|
-
|
|
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)',
|