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
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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)',
|