hedgequantx 2.5.4 → 2.5.5
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 +244 -27
- package/src/services/ai/providers/index.js +1 -21
- package/src/services/ai/token-scanner.js +982 -0
package/package.json
CHANGED
package/src/menus/ai-agent.js
CHANGED
|
@@ -10,6 +10,7 @@ const { getLogoWidth, drawBoxHeader, drawBoxFooter, displayBanner } = require('.
|
|
|
10
10
|
const { prompts } = require('../utils');
|
|
11
11
|
const aiService = require('../services/ai');
|
|
12
12
|
const { getCategories, getProvidersByCategory } = require('../services/ai/providers');
|
|
13
|
+
const tokenScanner = require('../services/ai/token-scanner');
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Main AI Agent menu
|
|
@@ -67,7 +68,7 @@ const aiAgentMenu = async () => {
|
|
|
67
68
|
|
|
68
69
|
switch (choice?.toLowerCase()) {
|
|
69
70
|
case '1':
|
|
70
|
-
return await
|
|
71
|
+
return await showExistingTokens();
|
|
71
72
|
case '2':
|
|
72
73
|
if (connection) {
|
|
73
74
|
return await selectModel(connection.provider);
|
|
@@ -88,6 +89,129 @@ const aiAgentMenu = async () => {
|
|
|
88
89
|
}
|
|
89
90
|
};
|
|
90
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Show existing tokens found on the system
|
|
94
|
+
*/
|
|
95
|
+
const showExistingTokens = async () => {
|
|
96
|
+
const boxWidth = getLogoWidth();
|
|
97
|
+
const W = boxWidth - 2;
|
|
98
|
+
|
|
99
|
+
const makeLine = (content) => {
|
|
100
|
+
const plainLen = content.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
101
|
+
const padding = W - plainLen;
|
|
102
|
+
return chalk.cyan('║') + ' ' + content + ' '.repeat(Math.max(0, padding - 1)) + chalk.cyan('║');
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
console.clear();
|
|
106
|
+
displayBanner();
|
|
107
|
+
drawBoxHeader('SCANNING FOR EXISTING SESSIONS...', boxWidth);
|
|
108
|
+
console.log(makeLine(''));
|
|
109
|
+
console.log(makeLine(chalk.gray('CHECKING VS CODE, CURSOR, CLAUDE CLI, OPENCODE...')));
|
|
110
|
+
console.log(makeLine(''));
|
|
111
|
+
drawBoxFooter(boxWidth);
|
|
112
|
+
|
|
113
|
+
// Scan for tokens
|
|
114
|
+
const tokens = tokenScanner.scanAllSources();
|
|
115
|
+
|
|
116
|
+
if (tokens.length === 0) {
|
|
117
|
+
// No tokens found, go directly to category selection
|
|
118
|
+
return await selectCategory();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Show found tokens
|
|
122
|
+
console.clear();
|
|
123
|
+
displayBanner();
|
|
124
|
+
drawBoxHeader('EXISTING SESSIONS FOUND', boxWidth);
|
|
125
|
+
|
|
126
|
+
console.log(makeLine(chalk.green(`FOUND ${tokens.length} EXISTING SESSION(S)`)));
|
|
127
|
+
console.log(makeLine(''));
|
|
128
|
+
|
|
129
|
+
const formatted = tokenScanner.formatResults(tokens);
|
|
130
|
+
|
|
131
|
+
for (const t of formatted) {
|
|
132
|
+
const providerColor = t.provider.includes('CLAUDE') ? chalk.magenta :
|
|
133
|
+
t.provider.includes('OPENAI') ? chalk.green :
|
|
134
|
+
t.provider.includes('OPENROUTER') ? chalk.yellow : chalk.cyan;
|
|
135
|
+
|
|
136
|
+
console.log(makeLine(
|
|
137
|
+
chalk.white(`[${t.index}] `) +
|
|
138
|
+
providerColor(t.provider) +
|
|
139
|
+
chalk.gray(` (${t.type})`)
|
|
140
|
+
));
|
|
141
|
+
console.log(makeLine(
|
|
142
|
+
chalk.gray(` ${t.icon} ${t.source} - ${t.lastUsed}`)
|
|
143
|
+
));
|
|
144
|
+
console.log(makeLine(
|
|
145
|
+
chalk.gray(` TOKEN: ${t.tokenPreview}`)
|
|
146
|
+
));
|
|
147
|
+
console.log(makeLine(''));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.log(chalk.cyan('╠' + '═'.repeat(W) + '╣'));
|
|
151
|
+
console.log(makeLine(chalk.cyan('[N] CONNECT NEW PROVIDER')));
|
|
152
|
+
console.log(makeLine(chalk.gray('[<] BACK')));
|
|
153
|
+
|
|
154
|
+
drawBoxFooter(boxWidth);
|
|
155
|
+
|
|
156
|
+
const choice = await prompts.textInput(chalk.cyan('SELECT (1-' + tokens.length + '/N/<):'));
|
|
157
|
+
|
|
158
|
+
if (choice === '<' || choice?.toLowerCase() === 'b') {
|
|
159
|
+
return await aiAgentMenu();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (choice?.toLowerCase() === 'n') {
|
|
163
|
+
return await selectCategory();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const index = parseInt(choice) - 1;
|
|
167
|
+
if (isNaN(index) || index < 0 || index >= tokens.length) {
|
|
168
|
+
return await showExistingTokens();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Use selected token
|
|
172
|
+
const selectedToken = tokens[index];
|
|
173
|
+
|
|
174
|
+
const spinner = ora({ text: 'VALIDATING TOKEN...', color: 'cyan' }).start();
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
// Validate the token
|
|
178
|
+
const credentials = { apiKey: selectedToken.token };
|
|
179
|
+
const validation = await aiService.validateConnection(selectedToken.provider, 'api_key', credentials);
|
|
180
|
+
|
|
181
|
+
if (!validation.valid) {
|
|
182
|
+
spinner.fail(`TOKEN INVALID OR EXPIRED: ${validation.error}`);
|
|
183
|
+
await prompts.waitForEnter();
|
|
184
|
+
return await showExistingTokens();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Get provider info
|
|
188
|
+
const { getProvider } = require('../services/ai/providers');
|
|
189
|
+
const provider = getProvider(selectedToken.provider);
|
|
190
|
+
|
|
191
|
+
if (!provider) {
|
|
192
|
+
spinner.fail('PROVIDER NOT SUPPORTED');
|
|
193
|
+
await prompts.waitForEnter();
|
|
194
|
+
return await showExistingTokens();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Save connection
|
|
198
|
+
const model = provider.defaultModel;
|
|
199
|
+
await aiService.connect(selectedToken.provider, 'api_key', credentials, model);
|
|
200
|
+
|
|
201
|
+
spinner.succeed(`CONNECTED TO ${provider.name}`);
|
|
202
|
+
console.log(chalk.gray(` SOURCE: ${selectedToken.source}`));
|
|
203
|
+
console.log(chalk.gray(` MODEL: ${model}`));
|
|
204
|
+
|
|
205
|
+
await prompts.waitForEnter();
|
|
206
|
+
return await aiAgentMenu();
|
|
207
|
+
|
|
208
|
+
} catch (error) {
|
|
209
|
+
spinner.fail(`CONNECTION FAILED: ${error.message}`);
|
|
210
|
+
await prompts.waitForEnter();
|
|
211
|
+
return await showExistingTokens();
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
91
215
|
/**
|
|
92
216
|
* Select provider category
|
|
93
217
|
*/
|
|
@@ -323,6 +447,78 @@ const selectProviderOption = async (provider) => {
|
|
|
323
447
|
return await setupConnection(provider, selectedOption);
|
|
324
448
|
};
|
|
325
449
|
|
|
450
|
+
/**
|
|
451
|
+
* Open URL in default browser
|
|
452
|
+
*/
|
|
453
|
+
const openBrowser = (url) => {
|
|
454
|
+
const { exec } = require('child_process');
|
|
455
|
+
const platform = process.platform;
|
|
456
|
+
|
|
457
|
+
let cmd;
|
|
458
|
+
if (platform === 'darwin') cmd = `open "${url}"`;
|
|
459
|
+
else if (platform === 'win32') cmd = `start "" "${url}"`;
|
|
460
|
+
else cmd = `xdg-open "${url}"`;
|
|
461
|
+
|
|
462
|
+
exec(cmd, (err) => {
|
|
463
|
+
if (err) console.log(chalk.gray(' Could not open browser automatically'));
|
|
464
|
+
});
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Get instructions for each credential type
|
|
469
|
+
*/
|
|
470
|
+
const getCredentialInstructions = (provider, option, field) => {
|
|
471
|
+
const instructions = {
|
|
472
|
+
apiKey: {
|
|
473
|
+
title: 'API KEY REQUIRED',
|
|
474
|
+
steps: [
|
|
475
|
+
'1. CLICK THE LINK BELOW TO OPEN IN BROWSER',
|
|
476
|
+
'2. SIGN IN OR CREATE AN ACCOUNT',
|
|
477
|
+
'3. GENERATE A NEW API KEY',
|
|
478
|
+
'4. COPY AND PASTE IT HERE'
|
|
479
|
+
]
|
|
480
|
+
},
|
|
481
|
+
sessionKey: {
|
|
482
|
+
title: 'SESSION KEY REQUIRED (SUBSCRIPTION PLAN)',
|
|
483
|
+
steps: [
|
|
484
|
+
'1. OPEN THE LINK BELOW IN YOUR BROWSER',
|
|
485
|
+
'2. SIGN IN WITH YOUR SUBSCRIPTION ACCOUNT',
|
|
486
|
+
'3. OPEN DEVELOPER TOOLS (F12 OR CMD+OPT+I)',
|
|
487
|
+
'4. GO TO APPLICATION > COOKIES',
|
|
488
|
+
'5. FIND "sessionKey" OR SIMILAR TOKEN',
|
|
489
|
+
'6. COPY THE VALUE AND PASTE IT HERE'
|
|
490
|
+
]
|
|
491
|
+
},
|
|
492
|
+
accessToken: {
|
|
493
|
+
title: 'ACCESS TOKEN REQUIRED (SUBSCRIPTION PLAN)',
|
|
494
|
+
steps: [
|
|
495
|
+
'1. OPEN THE LINK BELOW IN YOUR BROWSER',
|
|
496
|
+
'2. SIGN IN WITH YOUR SUBSCRIPTION ACCOUNT',
|
|
497
|
+
'3. OPEN DEVELOPER TOOLS (F12 OR CMD+OPT+I)',
|
|
498
|
+
'4. GO TO APPLICATION > COOKIES OR LOCAL STORAGE',
|
|
499
|
+
'5. FIND "accessToken" OR "token"',
|
|
500
|
+
'6. COPY THE VALUE AND PASTE IT HERE'
|
|
501
|
+
]
|
|
502
|
+
},
|
|
503
|
+
endpoint: {
|
|
504
|
+
title: 'ENDPOINT URL',
|
|
505
|
+
steps: [
|
|
506
|
+
'1. ENTER THE API ENDPOINT URL',
|
|
507
|
+
'2. USUALLY http://localhost:PORT FOR LOCAL'
|
|
508
|
+
]
|
|
509
|
+
},
|
|
510
|
+
model: {
|
|
511
|
+
title: 'MODEL NAME',
|
|
512
|
+
steps: [
|
|
513
|
+
'1. ENTER THE MODEL NAME TO USE',
|
|
514
|
+
'2. CHECK PROVIDER DOCS FOR AVAILABLE MODELS'
|
|
515
|
+
]
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
return instructions[field] || { title: field.toUpperCase(), steps: [] };
|
|
520
|
+
};
|
|
521
|
+
|
|
326
522
|
/**
|
|
327
523
|
* Setup connection with credentials
|
|
328
524
|
*/
|
|
@@ -336,64 +532,85 @@ const setupConnection = async (provider, option) => {
|
|
|
336
532
|
return chalk.cyan('║') + ' ' + content + ' '.repeat(Math.max(0, padding - 1)) + chalk.cyan('║');
|
|
337
533
|
};
|
|
338
534
|
|
|
339
|
-
console.clear();
|
|
340
|
-
displayBanner();
|
|
341
|
-
drawBoxHeader(`CONNECT TO ${provider.name}`, boxWidth);
|
|
342
|
-
|
|
343
|
-
// Show instructions
|
|
344
|
-
if (option.url) {
|
|
345
|
-
console.log(makeLine(chalk.white('GET YOUR CREDENTIALS:')));
|
|
346
|
-
console.log(makeLine(chalk.cyan(option.url)));
|
|
347
|
-
console.log(makeLine(''));
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
console.log(makeLine(chalk.gray('TYPE < TO GO BACK')));
|
|
351
|
-
|
|
352
|
-
drawBoxFooter(boxWidth);
|
|
353
|
-
console.log();
|
|
354
|
-
|
|
355
535
|
// Collect credentials based on fields
|
|
356
536
|
const credentials = {};
|
|
357
537
|
|
|
358
538
|
for (const field of option.fields) {
|
|
539
|
+
// Show instructions for this field
|
|
540
|
+
console.clear();
|
|
541
|
+
displayBanner();
|
|
542
|
+
drawBoxHeader(`CONNECT TO ${provider.name}`, boxWidth);
|
|
543
|
+
|
|
544
|
+
const instructions = getCredentialInstructions(provider, option, field);
|
|
545
|
+
|
|
546
|
+
console.log(makeLine(chalk.yellow(instructions.title)));
|
|
547
|
+
console.log(makeLine(''));
|
|
548
|
+
|
|
549
|
+
// Show steps
|
|
550
|
+
for (const step of instructions.steps) {
|
|
551
|
+
console.log(makeLine(chalk.white(step)));
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
console.log(makeLine(''));
|
|
555
|
+
|
|
556
|
+
// Show URL and open browser
|
|
557
|
+
if (option.url && (field === 'apiKey' || field === 'sessionKey' || field === 'accessToken')) {
|
|
558
|
+
console.log(makeLine(chalk.cyan('LINK: ') + chalk.green(option.url)));
|
|
559
|
+
console.log(makeLine(''));
|
|
560
|
+
console.log(makeLine(chalk.gray('OPENING BROWSER...')));
|
|
561
|
+
openBrowser(option.url);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Show default for endpoint
|
|
565
|
+
if (field === 'endpoint' && option.defaultEndpoint) {
|
|
566
|
+
console.log(makeLine(chalk.gray(`DEFAULT: ${option.defaultEndpoint}`)));
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
console.log(makeLine(''));
|
|
570
|
+
console.log(makeLine(chalk.gray('TYPE < TO GO BACK')));
|
|
571
|
+
|
|
572
|
+
drawBoxFooter(boxWidth);
|
|
573
|
+
console.log();
|
|
574
|
+
|
|
359
575
|
let value;
|
|
360
576
|
|
|
361
577
|
switch (field) {
|
|
362
578
|
case 'apiKey':
|
|
363
|
-
value = await prompts.textInput('
|
|
579
|
+
value = await prompts.textInput(chalk.cyan('PASTE API KEY:'));
|
|
364
580
|
if (!value || value === '<') return await selectProviderOption(provider);
|
|
365
|
-
credentials.apiKey = value;
|
|
581
|
+
credentials.apiKey = value.trim();
|
|
366
582
|
break;
|
|
367
583
|
|
|
368
584
|
case 'sessionKey':
|
|
369
|
-
value = await prompts.textInput('
|
|
585
|
+
value = await prompts.textInput(chalk.cyan('PASTE SESSION KEY:'));
|
|
370
586
|
if (!value || value === '<') return await selectProviderOption(provider);
|
|
371
|
-
credentials.sessionKey = value;
|
|
587
|
+
credentials.sessionKey = value.trim();
|
|
372
588
|
break;
|
|
373
589
|
|
|
374
590
|
case 'accessToken':
|
|
375
|
-
value = await prompts.textInput('
|
|
591
|
+
value = await prompts.textInput(chalk.cyan('PASTE ACCESS TOKEN:'));
|
|
376
592
|
if (!value || value === '<') return await selectProviderOption(provider);
|
|
377
|
-
credentials.accessToken = value;
|
|
593
|
+
credentials.accessToken = value.trim();
|
|
378
594
|
break;
|
|
379
595
|
|
|
380
596
|
case 'endpoint':
|
|
381
597
|
const defaultEndpoint = option.defaultEndpoint || '';
|
|
382
|
-
value = await prompts.textInput(`ENDPOINT [${defaultEndpoint || 'required'}]
|
|
598
|
+
value = await prompts.textInput(chalk.cyan(`ENDPOINT [${defaultEndpoint || 'required'}]:`));
|
|
383
599
|
if (value === '<') return await selectProviderOption(provider);
|
|
384
|
-
credentials.endpoint = value || defaultEndpoint;
|
|
600
|
+
credentials.endpoint = (value || defaultEndpoint).trim();
|
|
385
601
|
if (!credentials.endpoint) return await selectProviderOption(provider);
|
|
386
602
|
break;
|
|
387
603
|
|
|
388
604
|
case 'model':
|
|
389
|
-
value = await prompts.textInput('MODEL NAME
|
|
605
|
+
value = await prompts.textInput(chalk.cyan('MODEL NAME:'));
|
|
390
606
|
if (!value || value === '<') return await selectProviderOption(provider);
|
|
391
|
-
credentials.model = value;
|
|
607
|
+
credentials.model = value.trim();
|
|
392
608
|
break;
|
|
393
609
|
}
|
|
394
610
|
}
|
|
395
611
|
|
|
396
612
|
// Validate connection
|
|
613
|
+
console.log();
|
|
397
614
|
const spinner = ora({ text: 'VALIDATING CONNECTION...', color: 'cyan' }).start();
|
|
398
615
|
|
|
399
616
|
const validation = await aiService.validateConnection(provider.id, option.id, credentials);
|
|
@@ -54,17 +54,7 @@ const PROVIDERS = {
|
|
|
54
54
|
'~$0.10 per trading session'
|
|
55
55
|
],
|
|
56
56
|
fields: ['apiKey'],
|
|
57
|
-
url: 'https://console.anthropic.com'
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
id: 'max_plan',
|
|
61
|
-
label: 'MAX PLAN ($100/MONTH)',
|
|
62
|
-
description: [
|
|
63
|
-
'Subscribe at claude.ai',
|
|
64
|
-
'Unlimited usage'
|
|
65
|
-
],
|
|
66
|
-
fields: ['sessionKey'],
|
|
67
|
-
url: 'https://claude.ai'
|
|
57
|
+
url: 'https://console.anthropic.com/settings/keys'
|
|
68
58
|
}
|
|
69
59
|
],
|
|
70
60
|
endpoint: 'https://api.anthropic.com/v1'
|
|
@@ -87,16 +77,6 @@ const PROVIDERS = {
|
|
|
87
77
|
],
|
|
88
78
|
fields: ['apiKey'],
|
|
89
79
|
url: 'https://platform.openai.com/api-keys'
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
id: 'plus_plan',
|
|
93
|
-
label: 'PLUS PLAN ($20/MONTH)',
|
|
94
|
-
description: [
|
|
95
|
-
'Subscribe at chat.openai.com',
|
|
96
|
-
'GPT-4 access included'
|
|
97
|
-
],
|
|
98
|
-
fields: ['accessToken'],
|
|
99
|
-
url: 'https://chat.openai.com'
|
|
100
80
|
}
|
|
101
81
|
],
|
|
102
82
|
endpoint: 'https://api.openai.com/v1'
|
|
@@ -0,0 +1,982 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Token Scanner - Ultra Solid Edition
|
|
3
|
+
* Scans for existing AI provider tokens from various IDEs, tools, and configs
|
|
4
|
+
* Supports macOS, Linux, Windows, and headless servers
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const { execSync } = require('child_process');
|
|
11
|
+
|
|
12
|
+
const homeDir = os.homedir();
|
|
13
|
+
const platform = process.platform; // 'darwin', 'linux', 'win32'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Detect if running on a headless server (no GUI)
|
|
17
|
+
*/
|
|
18
|
+
const isHeadlessServer = () => {
|
|
19
|
+
if (platform === 'win32') return false;
|
|
20
|
+
|
|
21
|
+
// Check for common server indicators
|
|
22
|
+
const indicators = [
|
|
23
|
+
!process.env.DISPLAY && !process.env.WAYLAND_DISPLAY, // No display server
|
|
24
|
+
process.env.SSH_CLIENT || process.env.SSH_TTY, // SSH session
|
|
25
|
+
process.env.TERM === 'dumb', // Dumb terminal
|
|
26
|
+
fs.existsSync('/etc/ssh/sshd_config'), // SSH server installed
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
// Check if running in container
|
|
30
|
+
const inContainer = fs.existsSync('/.dockerenv') ||
|
|
31
|
+
fs.existsSync('/run/.containerenv') ||
|
|
32
|
+
(fs.existsSync('/proc/1/cgroup') &&
|
|
33
|
+
fs.readFileSync('/proc/1/cgroup', 'utf8').includes('docker'));
|
|
34
|
+
|
|
35
|
+
return indicators.filter(Boolean).length >= 2 || inContainer;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get app data directory based on OS
|
|
40
|
+
*/
|
|
41
|
+
const getAppDataDir = () => {
|
|
42
|
+
switch (platform) {
|
|
43
|
+
case 'darwin':
|
|
44
|
+
return path.join(homeDir, 'Library', 'Application Support');
|
|
45
|
+
case 'win32':
|
|
46
|
+
return process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming');
|
|
47
|
+
case 'linux':
|
|
48
|
+
default:
|
|
49
|
+
return process.env.XDG_CONFIG_HOME || path.join(homeDir, '.config');
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get all possible config directories (for thorough scanning)
|
|
55
|
+
*/
|
|
56
|
+
const getAllConfigDirs = () => {
|
|
57
|
+
const dirs = [homeDir];
|
|
58
|
+
|
|
59
|
+
switch (platform) {
|
|
60
|
+
case 'darwin':
|
|
61
|
+
dirs.push(
|
|
62
|
+
path.join(homeDir, 'Library', 'Application Support'),
|
|
63
|
+
path.join(homeDir, 'Library', 'Preferences'),
|
|
64
|
+
path.join(homeDir, '.config')
|
|
65
|
+
);
|
|
66
|
+
break;
|
|
67
|
+
case 'win32':
|
|
68
|
+
dirs.push(
|
|
69
|
+
process.env.APPDATA,
|
|
70
|
+
process.env.LOCALAPPDATA,
|
|
71
|
+
path.join(homeDir, '.config')
|
|
72
|
+
);
|
|
73
|
+
break;
|
|
74
|
+
case 'linux':
|
|
75
|
+
default:
|
|
76
|
+
dirs.push(
|
|
77
|
+
path.join(homeDir, '.config'),
|
|
78
|
+
path.join(homeDir, '.local', 'share'),
|
|
79
|
+
'/etc' // System-wide configs (server)
|
|
80
|
+
);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return dirs.filter(d => d && pathExists(d));
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* IDE and tool configurations for token scanning
|
|
89
|
+
*/
|
|
90
|
+
const TOKEN_SOURCES = {
|
|
91
|
+
// ==================== VS CODE FAMILY ====================
|
|
92
|
+
vscode: {
|
|
93
|
+
name: 'VS CODE',
|
|
94
|
+
icon: '💻',
|
|
95
|
+
paths: {
|
|
96
|
+
darwin: [
|
|
97
|
+
path.join(getAppDataDir(), 'Code', 'User', 'globalStorage'),
|
|
98
|
+
path.join(getAppDataDir(), 'Code', 'User')
|
|
99
|
+
],
|
|
100
|
+
linux: [
|
|
101
|
+
path.join(homeDir, '.config', 'Code', 'User', 'globalStorage'),
|
|
102
|
+
path.join(homeDir, '.config', 'Code', 'User'),
|
|
103
|
+
path.join(homeDir, '.vscode')
|
|
104
|
+
],
|
|
105
|
+
win32: [
|
|
106
|
+
path.join(getAppDataDir(), 'Code', 'User', 'globalStorage'),
|
|
107
|
+
path.join(getAppDataDir(), 'Code', 'User')
|
|
108
|
+
]
|
|
109
|
+
},
|
|
110
|
+
extensions: {
|
|
111
|
+
claude: ['anthropic.claude-code', 'anthropic.claude'],
|
|
112
|
+
continue: ['continue.continue'],
|
|
113
|
+
cline: ['saoudrizwan.claude-dev'],
|
|
114
|
+
openai: ['openai.openai-chatgpt']
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
vscodeInsiders: {
|
|
119
|
+
name: 'VS CODE INSIDERS',
|
|
120
|
+
icon: '💻',
|
|
121
|
+
paths: {
|
|
122
|
+
darwin: [path.join(getAppDataDir(), 'Code - Insiders', 'User', 'globalStorage')],
|
|
123
|
+
linux: [path.join(homeDir, '.config', 'Code - Insiders', 'User', 'globalStorage')],
|
|
124
|
+
win32: [path.join(getAppDataDir(), 'Code - Insiders', 'User', 'globalStorage')]
|
|
125
|
+
},
|
|
126
|
+
extensions: {
|
|
127
|
+
claude: ['anthropic.claude-code', 'anthropic.claude'],
|
|
128
|
+
continue: ['continue.continue']
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
vscodium: {
|
|
133
|
+
name: 'VSCODIUM',
|
|
134
|
+
icon: '💻',
|
|
135
|
+
paths: {
|
|
136
|
+
darwin: [path.join(getAppDataDir(), 'VSCodium', 'User', 'globalStorage')],
|
|
137
|
+
linux: [path.join(homeDir, '.config', 'VSCodium', 'User', 'globalStorage')],
|
|
138
|
+
win32: [path.join(getAppDataDir(), 'VSCodium', 'User', 'globalStorage')]
|
|
139
|
+
},
|
|
140
|
+
extensions: {
|
|
141
|
+
claude: ['anthropic.claude-code'],
|
|
142
|
+
continue: ['continue.continue']
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
// ==================== AI-FOCUSED EDITORS ====================
|
|
147
|
+
cursor: {
|
|
148
|
+
name: 'CURSOR',
|
|
149
|
+
icon: '🖱️',
|
|
150
|
+
paths: {
|
|
151
|
+
darwin: [
|
|
152
|
+
path.join(getAppDataDir(), 'Cursor', 'User', 'globalStorage'),
|
|
153
|
+
path.join(getAppDataDir(), 'Cursor', 'User'),
|
|
154
|
+
path.join(homeDir, '.cursor')
|
|
155
|
+
],
|
|
156
|
+
linux: [
|
|
157
|
+
path.join(homeDir, '.config', 'Cursor', 'User', 'globalStorage'),
|
|
158
|
+
path.join(homeDir, '.cursor')
|
|
159
|
+
],
|
|
160
|
+
win32: [
|
|
161
|
+
path.join(getAppDataDir(), 'Cursor', 'User', 'globalStorage'),
|
|
162
|
+
path.join(homeDir, '.cursor')
|
|
163
|
+
]
|
|
164
|
+
},
|
|
165
|
+
extensions: {
|
|
166
|
+
claude: ['anthropic.claude-code'],
|
|
167
|
+
continue: ['continue.continue']
|
|
168
|
+
},
|
|
169
|
+
configFiles: ['config.json', 'settings.json', 'credentials.json']
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
windsurf: {
|
|
173
|
+
name: 'WINDSURF',
|
|
174
|
+
icon: '🏄',
|
|
175
|
+
paths: {
|
|
176
|
+
darwin: [
|
|
177
|
+
path.join(getAppDataDir(), 'Windsurf', 'User', 'globalStorage'),
|
|
178
|
+
path.join(getAppDataDir(), 'Windsurf', 'User')
|
|
179
|
+
],
|
|
180
|
+
linux: [
|
|
181
|
+
path.join(homeDir, '.config', 'Windsurf', 'User', 'globalStorage'),
|
|
182
|
+
path.join(homeDir, '.windsurf')
|
|
183
|
+
],
|
|
184
|
+
win32: [
|
|
185
|
+
path.join(getAppDataDir(), 'Windsurf', 'User', 'globalStorage')
|
|
186
|
+
]
|
|
187
|
+
},
|
|
188
|
+
extensions: {
|
|
189
|
+
claude: ['anthropic.claude-code']
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
zed: {
|
|
194
|
+
name: 'ZED',
|
|
195
|
+
icon: '⚡',
|
|
196
|
+
paths: {
|
|
197
|
+
darwin: [
|
|
198
|
+
path.join(getAppDataDir(), 'Zed'),
|
|
199
|
+
path.join(homeDir, '.zed')
|
|
200
|
+
],
|
|
201
|
+
linux: [
|
|
202
|
+
path.join(homeDir, '.config', 'zed'),
|
|
203
|
+
path.join(homeDir, '.zed')
|
|
204
|
+
],
|
|
205
|
+
win32: [
|
|
206
|
+
path.join(getAppDataDir(), 'Zed')
|
|
207
|
+
]
|
|
208
|
+
},
|
|
209
|
+
configFiles: ['settings.json', 'credentials.json', 'keychain.json']
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
// ==================== CLI TOOLS ====================
|
|
213
|
+
claudeCli: {
|
|
214
|
+
name: 'CLAUDE CLI',
|
|
215
|
+
icon: '🤖',
|
|
216
|
+
paths: {
|
|
217
|
+
darwin: [path.join(homeDir, '.claude')],
|
|
218
|
+
linux: [path.join(homeDir, '.claude')],
|
|
219
|
+
win32: [path.join(homeDir, '.claude')]
|
|
220
|
+
},
|
|
221
|
+
configFiles: ['.credentials.json', 'credentials.json', 'config.json', '.credentials', 'settings.json', 'settings.local.json', 'auth.json']
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
opencode: {
|
|
225
|
+
name: 'OPENCODE',
|
|
226
|
+
icon: '🔓',
|
|
227
|
+
paths: {
|
|
228
|
+
darwin: [path.join(homeDir, '.opencode')],
|
|
229
|
+
linux: [path.join(homeDir, '.opencode')],
|
|
230
|
+
win32: [path.join(homeDir, '.opencode')]
|
|
231
|
+
},
|
|
232
|
+
configFiles: ['config.json', 'credentials.json', 'settings.json', 'auth.json']
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
aider: {
|
|
236
|
+
name: 'AIDER',
|
|
237
|
+
icon: '🔧',
|
|
238
|
+
paths: {
|
|
239
|
+
darwin: [path.join(homeDir, '.aider')],
|
|
240
|
+
linux: [path.join(homeDir, '.aider')],
|
|
241
|
+
win32: [path.join(homeDir, '.aider')]
|
|
242
|
+
},
|
|
243
|
+
configFiles: ['config.yml', '.aider.conf.yml', 'credentials.json']
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
continuedev: {
|
|
247
|
+
name: 'CONTINUE.DEV',
|
|
248
|
+
icon: '▶️',
|
|
249
|
+
paths: {
|
|
250
|
+
darwin: [path.join(homeDir, '.continue')],
|
|
251
|
+
linux: [path.join(homeDir, '.continue')],
|
|
252
|
+
win32: [path.join(homeDir, '.continue')]
|
|
253
|
+
},
|
|
254
|
+
configFiles: ['config.json', 'config.yaml', 'credentials.json']
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
cline: {
|
|
258
|
+
name: 'CLINE',
|
|
259
|
+
icon: '📟',
|
|
260
|
+
paths: {
|
|
261
|
+
darwin: [
|
|
262
|
+
path.join(getAppDataDir(), 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev'),
|
|
263
|
+
path.join(homeDir, '.cline')
|
|
264
|
+
],
|
|
265
|
+
linux: [
|
|
266
|
+
path.join(homeDir, '.config', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev'),
|
|
267
|
+
path.join(homeDir, '.cline')
|
|
268
|
+
],
|
|
269
|
+
win32: [
|
|
270
|
+
path.join(getAppDataDir(), 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev')
|
|
271
|
+
]
|
|
272
|
+
},
|
|
273
|
+
configFiles: ['settings.json', 'config.json']
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
// ==================== ENVIRONMENT VARIABLES ====================
|
|
277
|
+
envVars: {
|
|
278
|
+
name: 'ENVIRONMENT',
|
|
279
|
+
icon: '🌍',
|
|
280
|
+
envKeys: [
|
|
281
|
+
'ANTHROPIC_API_KEY',
|
|
282
|
+
'CLAUDE_API_KEY',
|
|
283
|
+
'OPENAI_API_KEY',
|
|
284
|
+
'OPENROUTER_API_KEY',
|
|
285
|
+
'GOOGLE_API_KEY',
|
|
286
|
+
'GEMINI_API_KEY',
|
|
287
|
+
'GROQ_API_KEY',
|
|
288
|
+
'DEEPSEEK_API_KEY',
|
|
289
|
+
'MISTRAL_API_KEY',
|
|
290
|
+
'PERPLEXITY_API_KEY',
|
|
291
|
+
'TOGETHER_API_KEY',
|
|
292
|
+
'XAI_API_KEY',
|
|
293
|
+
'GROK_API_KEY'
|
|
294
|
+
]
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
// ==================== SHELL CONFIGS (dotfiles) ====================
|
|
298
|
+
shellConfigs: {
|
|
299
|
+
name: 'SHELL CONFIG',
|
|
300
|
+
icon: '🐚',
|
|
301
|
+
paths: {
|
|
302
|
+
darwin: [homeDir],
|
|
303
|
+
linux: [homeDir],
|
|
304
|
+
win32: [homeDir]
|
|
305
|
+
},
|
|
306
|
+
configFiles: [
|
|
307
|
+
'.bashrc', '.bash_profile', '.zshrc', '.zprofile',
|
|
308
|
+
'.profile', '.envrc', '.env', '.env.local',
|
|
309
|
+
'.config/fish/config.fish'
|
|
310
|
+
]
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
// ==================== SERVER-SPECIFIC (Linux) ====================
|
|
314
|
+
serverConfigs: {
|
|
315
|
+
name: 'SERVER CONFIG',
|
|
316
|
+
icon: '🖥️',
|
|
317
|
+
paths: {
|
|
318
|
+
linux: [
|
|
319
|
+
'/etc/environment',
|
|
320
|
+
'/etc/profile.d',
|
|
321
|
+
path.join(homeDir, '.config'),
|
|
322
|
+
'/opt'
|
|
323
|
+
]
|
|
324
|
+
},
|
|
325
|
+
configFiles: ['*.env', '*.conf', 'config.json', 'credentials.json']
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
// ==================== NPM/NODE CONFIGS ====================
|
|
329
|
+
npmConfigs: {
|
|
330
|
+
name: 'NPM CONFIG',
|
|
331
|
+
icon: '📦',
|
|
332
|
+
paths: {
|
|
333
|
+
darwin: [path.join(homeDir, '.npm'), path.join(homeDir, '.npmrc')],
|
|
334
|
+
linux: [path.join(homeDir, '.npm'), path.join(homeDir, '.npmrc')],
|
|
335
|
+
win32: [path.join(homeDir, '.npm'), path.join(getAppDataDir(), 'npm')]
|
|
336
|
+
},
|
|
337
|
+
configFiles: ['.npmrc', 'config.json']
|
|
338
|
+
},
|
|
339
|
+
|
|
340
|
+
// ==================== GIT CONFIGS ====================
|
|
341
|
+
gitConfigs: {
|
|
342
|
+
name: 'GIT CONFIG',
|
|
343
|
+
icon: '📂',
|
|
344
|
+
paths: {
|
|
345
|
+
darwin: [path.join(homeDir, '.config', 'git')],
|
|
346
|
+
linux: [path.join(homeDir, '.config', 'git')],
|
|
347
|
+
win32: [path.join(homeDir, '.config', 'git')]
|
|
348
|
+
},
|
|
349
|
+
configFiles: ['credentials', 'config']
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Provider patterns to search for in config files
|
|
355
|
+
*/
|
|
356
|
+
const PROVIDER_PATTERNS = {
|
|
357
|
+
anthropic: {
|
|
358
|
+
name: 'CLAUDE',
|
|
359
|
+
displayName: 'CLAUDE (ANTHROPIC)',
|
|
360
|
+
keyPatterns: [
|
|
361
|
+
/sk-ant-api\d{2}-[a-zA-Z0-9_-]{80,}/g, // New format API key
|
|
362
|
+
/sk-ant-[a-zA-Z0-9_-]{40,}/g, // Old format API key
|
|
363
|
+
],
|
|
364
|
+
sessionPatterns: [
|
|
365
|
+
/"sessionKey"\s*:\s*"([^"]+)"/gi,
|
|
366
|
+
/'sessionKey'\s*:\s*'([^']+)'/gi,
|
|
367
|
+
/sessionKey\s*[=:]\s*['"]?([a-zA-Z0-9_-]{20,})['"]?/gi,
|
|
368
|
+
/claude[_-]?session[_-]?key\s*[=:]\s*['"]?([^'"}\s]+)['"]?/gi,
|
|
369
|
+
/claude[_-]?session\s*[=:]\s*['"]?([^'"}\s]+)['"]?/gi
|
|
370
|
+
],
|
|
371
|
+
envKey: 'ANTHROPIC_API_KEY'
|
|
372
|
+
},
|
|
373
|
+
|
|
374
|
+
openai: {
|
|
375
|
+
name: 'OPENAI',
|
|
376
|
+
displayName: 'OPENAI (GPT)',
|
|
377
|
+
keyPatterns: [
|
|
378
|
+
/sk-proj-[a-zA-Z0-9_-]{100,}/g, // Project API key (new)
|
|
379
|
+
/sk-(?!ant|or)[a-zA-Z0-9]{48,}/g, // Standard API key (NOT anthropic/openrouter)
|
|
380
|
+
],
|
|
381
|
+
sessionPatterns: [
|
|
382
|
+
/openai[_-]?accessToken\s*[=:]\s*['"]?([^'"}\s]+)['"]?/gi,
|
|
383
|
+
/chatgpt[_-]?session\s*[=:]\s*['"]?([^'"}\s]+)['"]?/gi
|
|
384
|
+
],
|
|
385
|
+
envKey: 'OPENAI_API_KEY'
|
|
386
|
+
},
|
|
387
|
+
|
|
388
|
+
openrouter: {
|
|
389
|
+
name: 'OPENROUTER',
|
|
390
|
+
displayName: 'OPENROUTER',
|
|
391
|
+
keyPatterns: [
|
|
392
|
+
/sk-or-v1-[a-zA-Z0-9]{64}/g, // OpenRouter API key
|
|
393
|
+
/sk-or-[a-zA-Z0-9_-]{40,}/g, // Alt format
|
|
394
|
+
],
|
|
395
|
+
envKey: 'OPENROUTER_API_KEY'
|
|
396
|
+
},
|
|
397
|
+
|
|
398
|
+
gemini: {
|
|
399
|
+
name: 'GEMINI',
|
|
400
|
+
displayName: 'GEMINI (GOOGLE)',
|
|
401
|
+
keyPatterns: [
|
|
402
|
+
/AIza[a-zA-Z0-9_-]{35}/g, // Google API key
|
|
403
|
+
],
|
|
404
|
+
envKey: 'GOOGLE_API_KEY'
|
|
405
|
+
},
|
|
406
|
+
|
|
407
|
+
groq: {
|
|
408
|
+
name: 'GROQ',
|
|
409
|
+
displayName: 'GROQ',
|
|
410
|
+
keyPatterns: [
|
|
411
|
+
/gsk_[a-zA-Z0-9]{52}/g, // Groq API key
|
|
412
|
+
],
|
|
413
|
+
envKey: 'GROQ_API_KEY'
|
|
414
|
+
},
|
|
415
|
+
|
|
416
|
+
deepseek: {
|
|
417
|
+
name: 'DEEPSEEK',
|
|
418
|
+
displayName: 'DEEPSEEK',
|
|
419
|
+
keyPatterns: [
|
|
420
|
+
/sk-[a-f0-9]{32}/g, // DeepSeek API key
|
|
421
|
+
],
|
|
422
|
+
envKey: 'DEEPSEEK_API_KEY'
|
|
423
|
+
},
|
|
424
|
+
|
|
425
|
+
mistral: {
|
|
426
|
+
name: 'MISTRAL',
|
|
427
|
+
displayName: 'MISTRAL',
|
|
428
|
+
keyPatterns: [
|
|
429
|
+
/mistral[_-]?[a-zA-Z0-9]{32}/gi, // Mistral key with prefix
|
|
430
|
+
],
|
|
431
|
+
envKey: 'MISTRAL_API_KEY'
|
|
432
|
+
},
|
|
433
|
+
|
|
434
|
+
perplexity: {
|
|
435
|
+
name: 'PERPLEXITY',
|
|
436
|
+
displayName: 'PERPLEXITY',
|
|
437
|
+
keyPatterns: [
|
|
438
|
+
/pplx-[a-zA-Z0-9]{48}/g, // Perplexity API key
|
|
439
|
+
],
|
|
440
|
+
envKey: 'PERPLEXITY_API_KEY'
|
|
441
|
+
},
|
|
442
|
+
|
|
443
|
+
together: {
|
|
444
|
+
name: 'TOGETHER',
|
|
445
|
+
displayName: 'TOGETHER AI',
|
|
446
|
+
keyPatterns: [
|
|
447
|
+
/together[_-]?[a-f0-9]{64}/gi, // Together API key with prefix
|
|
448
|
+
],
|
|
449
|
+
envKey: 'TOGETHER_API_KEY'
|
|
450
|
+
},
|
|
451
|
+
|
|
452
|
+
xai: {
|
|
453
|
+
name: 'XAI',
|
|
454
|
+
displayName: 'GROK (XAI)',
|
|
455
|
+
keyPatterns: [
|
|
456
|
+
/xai-[a-zA-Z0-9_-]{40,}/g, // xAI key
|
|
457
|
+
],
|
|
458
|
+
envKey: 'XAI_API_KEY'
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
// ==================== UTILITY FUNCTIONS ====================
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Check if a path exists
|
|
466
|
+
*/
|
|
467
|
+
const pathExists = (p) => {
|
|
468
|
+
try {
|
|
469
|
+
fs.accessSync(p);
|
|
470
|
+
return true;
|
|
471
|
+
} catch {
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Read file safely
|
|
478
|
+
*/
|
|
479
|
+
const readFileSafe = (filePath) => {
|
|
480
|
+
try {
|
|
481
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
482
|
+
} catch {
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Get file modification time
|
|
489
|
+
*/
|
|
490
|
+
const getFileModTime = (filePath) => {
|
|
491
|
+
try {
|
|
492
|
+
const stats = fs.statSync(filePath);
|
|
493
|
+
return stats.mtime;
|
|
494
|
+
} catch {
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* List files in directory (recursive optional)
|
|
501
|
+
*/
|
|
502
|
+
const listFiles = (dir, recursive = false, maxDepth = 3, currentDepth = 0) => {
|
|
503
|
+
if (!pathExists(dir) || currentDepth > maxDepth) return [];
|
|
504
|
+
|
|
505
|
+
try {
|
|
506
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
507
|
+
let files = [];
|
|
508
|
+
|
|
509
|
+
for (const entry of entries) {
|
|
510
|
+
const fullPath = path.join(dir, entry.name);
|
|
511
|
+
|
|
512
|
+
// Skip hidden system directories
|
|
513
|
+
if (entry.name.startsWith('.') && entry.isDirectory() &&
|
|
514
|
+
!['config', '.continue', '.claude', '.opencode', '.cursor', '.zed', '.aider'].some(n => entry.name.includes(n))) {
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (entry.isFile()) {
|
|
519
|
+
files.push(fullPath);
|
|
520
|
+
} else if (entry.isDirectory() && recursive) {
|
|
521
|
+
files = files.concat(listFiles(fullPath, recursive, maxDepth, currentDepth + 1));
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return files;
|
|
526
|
+
} catch {
|
|
527
|
+
return [];
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Validate token format
|
|
533
|
+
*/
|
|
534
|
+
const validateToken = (token, providerId) => {
|
|
535
|
+
if (!token || token.length < 10) return false;
|
|
536
|
+
|
|
537
|
+
// Check against known patterns
|
|
538
|
+
const provider = PROVIDER_PATTERNS[providerId];
|
|
539
|
+
if (!provider) return true; // Accept if no pattern defined
|
|
540
|
+
|
|
541
|
+
for (const pattern of provider.keyPatterns) {
|
|
542
|
+
// Reset regex state
|
|
543
|
+
pattern.lastIndex = 0;
|
|
544
|
+
if (pattern.test(token)) return true;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (provider.sessionPatterns) {
|
|
548
|
+
for (const pattern of provider.sessionPatterns) {
|
|
549
|
+
pattern.lastIndex = 0;
|
|
550
|
+
if (pattern.test(token)) return true;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Generic validation for session tokens
|
|
555
|
+
if (token.length > 20 && /^[a-zA-Z0-9_-]+$/.test(token)) {
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return false;
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
// ==================== SCANNING FUNCTIONS ====================
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Scan environment variables for API keys
|
|
566
|
+
*/
|
|
567
|
+
const scanEnvironmentVariables = () => {
|
|
568
|
+
const results = [];
|
|
569
|
+
|
|
570
|
+
for (const [providerId, provider] of Object.entries(PROVIDER_PATTERNS)) {
|
|
571
|
+
const envKey = provider.envKey;
|
|
572
|
+
if (envKey && process.env[envKey]) {
|
|
573
|
+
const token = process.env[envKey];
|
|
574
|
+
if (validateToken(token, providerId)) {
|
|
575
|
+
results.push({
|
|
576
|
+
source: 'ENVIRONMENT',
|
|
577
|
+
sourceId: 'envVars',
|
|
578
|
+
icon: '🌍',
|
|
579
|
+
type: 'api_key',
|
|
580
|
+
provider: providerId,
|
|
581
|
+
providerName: provider.displayName,
|
|
582
|
+
token: token,
|
|
583
|
+
filePath: `$${envKey}`,
|
|
584
|
+
lastUsed: new Date() // Env vars are "current"
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Also check generic key names
|
|
591
|
+
const genericEnvKeys = ['AI_API_KEY', 'LLM_API_KEY', 'API_KEY'];
|
|
592
|
+
for (const key of genericEnvKeys) {
|
|
593
|
+
if (process.env[key]) {
|
|
594
|
+
// Try to identify the provider
|
|
595
|
+
const token = process.env[key];
|
|
596
|
+
for (const [providerId, provider] of Object.entries(PROVIDER_PATTERNS)) {
|
|
597
|
+
for (const pattern of provider.keyPatterns) {
|
|
598
|
+
pattern.lastIndex = 0;
|
|
599
|
+
if (pattern.test(token)) {
|
|
600
|
+
results.push({
|
|
601
|
+
source: 'ENVIRONMENT',
|
|
602
|
+
sourceId: 'envVars',
|
|
603
|
+
icon: '🌍',
|
|
604
|
+
type: 'api_key',
|
|
605
|
+
provider: providerId,
|
|
606
|
+
providerName: provider.displayName,
|
|
607
|
+
token: token,
|
|
608
|
+
filePath: `$${key}`,
|
|
609
|
+
lastUsed: new Date()
|
|
610
|
+
});
|
|
611
|
+
break;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
return results;
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Search for tokens in a content string
|
|
623
|
+
*/
|
|
624
|
+
const searchTokensInContent = (content, filePath = null) => {
|
|
625
|
+
const results = [];
|
|
626
|
+
|
|
627
|
+
for (const [providerId, provider] of Object.entries(PROVIDER_PATTERNS)) {
|
|
628
|
+
// Search for API keys
|
|
629
|
+
for (const pattern of provider.keyPatterns) {
|
|
630
|
+
pattern.lastIndex = 0;
|
|
631
|
+
let match;
|
|
632
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
633
|
+
const token = match[0];
|
|
634
|
+
if (token.length > 10 && validateToken(token, providerId)) {
|
|
635
|
+
results.push({
|
|
636
|
+
type: 'api_key',
|
|
637
|
+
provider: providerId,
|
|
638
|
+
providerName: provider.displayName,
|
|
639
|
+
token: token
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Search for session tokens
|
|
646
|
+
if (provider.sessionPatterns) {
|
|
647
|
+
for (const pattern of provider.sessionPatterns) {
|
|
648
|
+
pattern.lastIndex = 0;
|
|
649
|
+
let match;
|
|
650
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
651
|
+
const token = match[1] || match[0];
|
|
652
|
+
if (token.length > 10) {
|
|
653
|
+
results.push({
|
|
654
|
+
type: 'session',
|
|
655
|
+
provider: providerId,
|
|
656
|
+
providerName: provider.displayName,
|
|
657
|
+
token: token
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return results;
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Parse JSON config file and extract tokens
|
|
670
|
+
*/
|
|
671
|
+
const parseJsonConfig = (content, filePath = null) => {
|
|
672
|
+
const results = [];
|
|
673
|
+
|
|
674
|
+
try {
|
|
675
|
+
const json = JSON.parse(content);
|
|
676
|
+
|
|
677
|
+
const extractFromObject = (obj, prefix = '') => {
|
|
678
|
+
if (!obj || typeof obj !== 'object') return;
|
|
679
|
+
|
|
680
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
681
|
+
const lowerKey = key.toLowerCase();
|
|
682
|
+
|
|
683
|
+
if (typeof value === 'string' && value.length > 10) {
|
|
684
|
+
// Check if key looks like a credential key
|
|
685
|
+
const isCredKey = [
|
|
686
|
+
'apikey', 'api_key', 'api-key',
|
|
687
|
+
'sessionkey', 'session_key', 'session-key',
|
|
688
|
+
'accesstoken', 'access_token', 'access-token',
|
|
689
|
+
'token', 'secret', 'credential', 'auth',
|
|
690
|
+
'anthropic', 'openai', 'claude', 'gpt'
|
|
691
|
+
].some(k => lowerKey.includes(k));
|
|
692
|
+
|
|
693
|
+
if (isCredKey) {
|
|
694
|
+
// Identify provider from token format
|
|
695
|
+
for (const [providerId, provider] of Object.entries(PROVIDER_PATTERNS)) {
|
|
696
|
+
for (const pattern of provider.keyPatterns) {
|
|
697
|
+
pattern.lastIndex = 0;
|
|
698
|
+
if (pattern.test(value)) {
|
|
699
|
+
results.push({
|
|
700
|
+
type: 'api_key',
|
|
701
|
+
provider: providerId,
|
|
702
|
+
providerName: provider.displayName,
|
|
703
|
+
token: value,
|
|
704
|
+
keyPath: prefix + key
|
|
705
|
+
});
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (provider.sessionPatterns) {
|
|
711
|
+
for (const pattern of provider.sessionPatterns) {
|
|
712
|
+
pattern.lastIndex = 0;
|
|
713
|
+
if (pattern.test(value) || pattern.test(`"${key}":"${value}"`)) {
|
|
714
|
+
results.push({
|
|
715
|
+
type: 'session',
|
|
716
|
+
provider: providerId,
|
|
717
|
+
providerName: provider.displayName,
|
|
718
|
+
token: value,
|
|
719
|
+
keyPath: prefix + key
|
|
720
|
+
});
|
|
721
|
+
break;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
728
|
+
extractFromObject(value, prefix + key + '.');
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
extractFromObject(json);
|
|
734
|
+
} catch {
|
|
735
|
+
// Not valid JSON, use regex search
|
|
736
|
+
return searchTokensInContent(content, filePath);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
return results;
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Scan a single source for tokens
|
|
744
|
+
*/
|
|
745
|
+
const scanSource = (sourceId) => {
|
|
746
|
+
const source = TOKEN_SOURCES[sourceId];
|
|
747
|
+
if (!source) return [];
|
|
748
|
+
|
|
749
|
+
const results = [];
|
|
750
|
+
const paths = source.paths?.[platform] || [];
|
|
751
|
+
|
|
752
|
+
// Scan each path
|
|
753
|
+
for (const basePath of paths) {
|
|
754
|
+
if (!pathExists(basePath)) continue;
|
|
755
|
+
|
|
756
|
+
// Scan extension directories (for VS Code-based editors)
|
|
757
|
+
if (source.extensions) {
|
|
758
|
+
for (const [providerHint, extIds] of Object.entries(source.extensions)) {
|
|
759
|
+
const extIdList = Array.isArray(extIds) ? extIds : [extIds];
|
|
760
|
+
|
|
761
|
+
for (const extId of extIdList) {
|
|
762
|
+
const extPath = path.join(basePath, extId);
|
|
763
|
+
if (!pathExists(extPath)) continue;
|
|
764
|
+
|
|
765
|
+
// Scan all files in extension directory
|
|
766
|
+
const files = listFiles(extPath, true, 2);
|
|
767
|
+
for (const filePath of files) {
|
|
768
|
+
const content = readFileSafe(filePath);
|
|
769
|
+
if (!content) continue;
|
|
770
|
+
|
|
771
|
+
const tokens = filePath.endsWith('.json')
|
|
772
|
+
? parseJsonConfig(content, filePath)
|
|
773
|
+
: searchTokensInContent(content, filePath);
|
|
774
|
+
|
|
775
|
+
for (const token of tokens) {
|
|
776
|
+
results.push({
|
|
777
|
+
source: source.name,
|
|
778
|
+
sourceId: sourceId,
|
|
779
|
+
icon: source.icon || '📁',
|
|
780
|
+
...token,
|
|
781
|
+
filePath: filePath,
|
|
782
|
+
lastUsed: getFileModTime(filePath)
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Scan config files
|
|
791
|
+
if (source.configFiles) {
|
|
792
|
+
for (const file of source.configFiles) {
|
|
793
|
+
// Handle wildcards
|
|
794
|
+
if (file.includes('*')) {
|
|
795
|
+
const files = listFiles(basePath, false);
|
|
796
|
+
const regex = new RegExp('^' + file.replace(/\*/g, '.*') + '$');
|
|
797
|
+
for (const f of files) {
|
|
798
|
+
if (regex.test(path.basename(f))) {
|
|
799
|
+
const content = readFileSafe(f);
|
|
800
|
+
if (content) {
|
|
801
|
+
const tokens = f.endsWith('.json')
|
|
802
|
+
? parseJsonConfig(content, f)
|
|
803
|
+
: searchTokensInContent(content, f);
|
|
804
|
+
|
|
805
|
+
for (const token of tokens) {
|
|
806
|
+
results.push({
|
|
807
|
+
source: source.name,
|
|
808
|
+
sourceId: sourceId,
|
|
809
|
+
icon: source.icon || '📁',
|
|
810
|
+
...token,
|
|
811
|
+
filePath: f,
|
|
812
|
+
lastUsed: getFileModTime(f)
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
} else {
|
|
819
|
+
const filePath = path.join(basePath, file);
|
|
820
|
+
const content = readFileSafe(filePath);
|
|
821
|
+
if (content) {
|
|
822
|
+
const tokens = filePath.endsWith('.json')
|
|
823
|
+
? parseJsonConfig(content, filePath)
|
|
824
|
+
: searchTokensInContent(content, filePath);
|
|
825
|
+
|
|
826
|
+
for (const token of tokens) {
|
|
827
|
+
results.push({
|
|
828
|
+
source: source.name,
|
|
829
|
+
sourceId: sourceId,
|
|
830
|
+
icon: source.icon || '📁',
|
|
831
|
+
...token,
|
|
832
|
+
filePath: filePath,
|
|
833
|
+
lastUsed: getFileModTime(filePath)
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
return results;
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Scan all sources for tokens
|
|
847
|
+
*/
|
|
848
|
+
const scanAllSources = () => {
|
|
849
|
+
const allResults = [];
|
|
850
|
+
|
|
851
|
+
// First, scan environment variables (highest priority)
|
|
852
|
+
allResults.push(...scanEnvironmentVariables());
|
|
853
|
+
|
|
854
|
+
// Then scan all tool sources
|
|
855
|
+
for (const sourceId of Object.keys(TOKEN_SOURCES)) {
|
|
856
|
+
if (sourceId === 'envVars') continue; // Already scanned
|
|
857
|
+
|
|
858
|
+
try {
|
|
859
|
+
const results = scanSource(sourceId);
|
|
860
|
+
allResults.push(...results);
|
|
861
|
+
} catch (err) {
|
|
862
|
+
// Silent fail for individual sources
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Remove duplicates (same token from multiple sources)
|
|
867
|
+
const uniqueTokens = new Map();
|
|
868
|
+
for (const result of allResults) {
|
|
869
|
+
if (result.token) {
|
|
870
|
+
const key = `${result.provider}:${result.token}`;
|
|
871
|
+
if (!uniqueTokens.has(key)) {
|
|
872
|
+
uniqueTokens.set(key, result);
|
|
873
|
+
} else {
|
|
874
|
+
// Keep the more recent one
|
|
875
|
+
const existing = uniqueTokens.get(key);
|
|
876
|
+
if (result.lastUsed && (!existing.lastUsed || result.lastUsed > existing.lastUsed)) {
|
|
877
|
+
uniqueTokens.set(key, result);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Sort by last used (most recent first)
|
|
884
|
+
return Array.from(uniqueTokens.values()).sort((a, b) => {
|
|
885
|
+
if (!a.lastUsed) return 1;
|
|
886
|
+
if (!b.lastUsed) return -1;
|
|
887
|
+
return b.lastUsed - a.lastUsed;
|
|
888
|
+
});
|
|
889
|
+
};
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Scan for a specific provider's tokens
|
|
893
|
+
*/
|
|
894
|
+
const scanForProvider = (providerId) => {
|
|
895
|
+
const allTokens = scanAllSources();
|
|
896
|
+
return allTokens.filter(t => t.provider === providerId);
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Get human-readable time ago
|
|
901
|
+
*/
|
|
902
|
+
const timeAgo = (date) => {
|
|
903
|
+
if (!date) return 'UNKNOWN';
|
|
904
|
+
|
|
905
|
+
const seconds = Math.floor((new Date() - date) / 1000);
|
|
906
|
+
|
|
907
|
+
if (seconds < 60) return 'JUST NOW';
|
|
908
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)} MIN AGO`;
|
|
909
|
+
if (seconds < 86400) return `${Math.floor(seconds / 3600)} HOURS AGO`;
|
|
910
|
+
if (seconds < 604800) return `${Math.floor(seconds / 86400)} DAYS AGO`;
|
|
911
|
+
if (seconds < 2592000) return `${Math.floor(seconds / 604800)} WEEKS AGO`;
|
|
912
|
+
return `${Math.floor(seconds / 2592000)} MONTHS AGO`;
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Format scan results for display
|
|
917
|
+
*/
|
|
918
|
+
const formatResults = (results) => {
|
|
919
|
+
return results.map((r, i) => ({
|
|
920
|
+
index: i + 1,
|
|
921
|
+
source: r.source,
|
|
922
|
+
icon: r.icon || '📁',
|
|
923
|
+
provider: r.providerName || PROVIDER_PATTERNS[r.provider]?.displayName || r.provider.toUpperCase(),
|
|
924
|
+
type: r.type === 'session' ? 'SESSION' : 'API KEY',
|
|
925
|
+
lastUsed: timeAgo(r.lastUsed),
|
|
926
|
+
tokenPreview: r.token ? `${r.token.substring(0, 10)}...${r.token.substring(r.token.length - 4)}` : 'N/A'
|
|
927
|
+
}));
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* Quick check if any tokens exist (fast scan)
|
|
932
|
+
*/
|
|
933
|
+
const hasExistingTokens = () => {
|
|
934
|
+
// Quick check environment variables first
|
|
935
|
+
for (const provider of Object.values(PROVIDER_PATTERNS)) {
|
|
936
|
+
if (provider.envKey && process.env[provider.envKey]) {
|
|
937
|
+
return true;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Quick check common locations
|
|
942
|
+
const quickPaths = [
|
|
943
|
+
path.join(homeDir, '.claude'),
|
|
944
|
+
path.join(homeDir, '.opencode'),
|
|
945
|
+
path.join(homeDir, '.continue')
|
|
946
|
+
];
|
|
947
|
+
|
|
948
|
+
for (const p of quickPaths) {
|
|
949
|
+
if (pathExists(p)) return true;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
return false;
|
|
953
|
+
};
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* Get system info for debugging
|
|
957
|
+
*/
|
|
958
|
+
const getSystemInfo = () => {
|
|
959
|
+
return {
|
|
960
|
+
platform,
|
|
961
|
+
homeDir,
|
|
962
|
+
appDataDir: getAppDataDir(),
|
|
963
|
+
isHeadless: isHeadlessServer(),
|
|
964
|
+
nodeVersion: process.version,
|
|
965
|
+
arch: os.arch()
|
|
966
|
+
};
|
|
967
|
+
};
|
|
968
|
+
|
|
969
|
+
module.exports = {
|
|
970
|
+
TOKEN_SOURCES,
|
|
971
|
+
PROVIDER_PATTERNS,
|
|
972
|
+
scanAllSources,
|
|
973
|
+
scanForProvider,
|
|
974
|
+
scanSource,
|
|
975
|
+
scanEnvironmentVariables,
|
|
976
|
+
formatResults,
|
|
977
|
+
timeAgo,
|
|
978
|
+
hasExistingTokens,
|
|
979
|
+
isHeadlessServer,
|
|
980
|
+
getSystemInfo,
|
|
981
|
+
validateToken
|
|
982
|
+
};
|