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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgequantx",
3
- "version": "2.5.4",
3
+ "version": "2.5.5",
4
4
  "description": "HedgeQuantX - Prop Futures Trading CLI",
5
5
  "main": "src/app.js",
6
6
  "bin": {
@@ -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 selectCategory();
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('ENTER API KEY (OR < TO GO BACK):');
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('ENTER SESSION KEY (OR < TO GO BACK):');
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('ENTER ACCESS TOKEN (OR < TO GO BACK):');
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'}] (OR < TO GO BACK):`);
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 (OR < TO GO BACK):');
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
+ };