veil-browser 0.2.1 ā 0.4.0
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/README.md +136 -84
- package/dist/browser.d.ts +0 -6
- package/dist/browser.js +47 -35
- package/dist/index.js +211 -16
- package/dist/platforms.d.ts +25 -0
- package/dist/platforms.js +281 -0
- package/package.json +1 -1
- package/dist/ai.js +0 -377
- package/dist/captcha.js +0 -203
- package/dist/mcp.js +0 -232
- package/dist/startup.js +0 -9
- package/dist/state.js +0 -45
package/dist/index.js
CHANGED
|
@@ -3,31 +3,81 @@ import { Command } from 'commander';
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { ensureBrowser, closeBrowser, getPage } from './browser.js';
|
|
5
5
|
import { saveSession } from './session.js';
|
|
6
|
+
import { getPlatform, listPlatforms, searchPlatforms } from './platforms.js';
|
|
6
7
|
const program = new Command();
|
|
7
8
|
program
|
|
8
9
|
.name('veil')
|
|
9
10
|
.description('š¶ļø OpenClaw browser remote ā stealth headless browser')
|
|
10
|
-
.version('0.
|
|
11
|
+
.version('0.4.0');
|
|
12
|
+
// āāā Platforms Directory āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
13
|
+
program
|
|
14
|
+
.command('platforms')
|
|
15
|
+
.description('Manage platform directory')
|
|
16
|
+
.action(() => {
|
|
17
|
+
console.log(chalk.blue('\nš Veil Platform Directory\n'));
|
|
18
|
+
const platforms = listPlatforms();
|
|
19
|
+
const byCategory = platforms.reduce((acc, p) => {
|
|
20
|
+
acc[p.category] = (acc[p.category] || []).concat(p);
|
|
21
|
+
return acc;
|
|
22
|
+
}, {});
|
|
23
|
+
for (const [category, proms] of Object.entries(byCategory)) {
|
|
24
|
+
console.log(chalk.cyan(` ${category.toUpperCase()} (${proms.length})`));
|
|
25
|
+
proms.forEach(p => {
|
|
26
|
+
console.log(chalk.gray(` ⢠${p.name} ā ${p.aliases.join(', ')}`));
|
|
27
|
+
});
|
|
28
|
+
console.log();
|
|
29
|
+
}
|
|
30
|
+
console.log(chalk.gray(`Total: ${platforms.length} platforms\n`));
|
|
31
|
+
});
|
|
32
|
+
program
|
|
33
|
+
.command('platforms:list [category]')
|
|
34
|
+
.description('List platforms by category (ai, social, dev, productivity, finance, shopping, email, other)')
|
|
35
|
+
.action((category) => {
|
|
36
|
+
const platforms = listPlatforms(category);
|
|
37
|
+
console.log(chalk.blue(`\nš ${category ? `${category.toUpperCase()} Platforms` : 'All Platforms'} (${platforms.length})\n`));
|
|
38
|
+
platforms.forEach(p => {
|
|
39
|
+
console.log(` ${chalk.cyan(p.name)}`);
|
|
40
|
+
console.log(` Aliases: ${p.aliases.join(', ')}`);
|
|
41
|
+
if (p.notes)
|
|
42
|
+
console.log(` Notes: ${chalk.gray(p.notes)}`);
|
|
43
|
+
console.log();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
program
|
|
47
|
+
.command('platforms:search <query>')
|
|
48
|
+
.description('Search for a platform')
|
|
49
|
+
.action((query) => {
|
|
50
|
+
const results = searchPlatforms(query);
|
|
51
|
+
if (results.length === 0) {
|
|
52
|
+
console.log(chalk.yellow(`\nNo platforms found matching "${query}"\n`));
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
console.log(chalk.blue(`\nš Found ${results.length} platform(s):\n`));
|
|
56
|
+
results.forEach(p => {
|
|
57
|
+
console.log(` ${chalk.cyan(p.name)}`);
|
|
58
|
+
console.log(` Login: ${chalk.gray(p.loginUrl)}`);
|
|
59
|
+
console.log(` Aliases: ${p.aliases.join(', ')}`);
|
|
60
|
+
console.log();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
});
|
|
11
64
|
// āāā Session āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
12
65
|
program
|
|
13
66
|
.command('login <platform>')
|
|
14
|
-
.description('Open visible browser to log in and save session
|
|
15
|
-
.action(async (
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
bluesky: 'https://bsky.app',
|
|
22
|
-
};
|
|
23
|
-
const url = platformUrls[platform.toLowerCase()] ?? `https://${platform}`;
|
|
24
|
-
const { browser, context, page } = await ensureBrowser({ headed: true, platform });
|
|
67
|
+
.description('Open visible browser to log in and save session')
|
|
68
|
+
.action(async (platformQuery) => {
|
|
69
|
+
const platform = getPlatform(platformQuery);
|
|
70
|
+
const url = platformQuery.startsWith('http://') || platformQuery.startsWith('https://')
|
|
71
|
+
? platformQuery
|
|
72
|
+
: platform?.loginUrl ?? `https://${platformQuery}`;
|
|
73
|
+
const { browser, context, page } = await ensureBrowser({ headed: true, platform: platformQuery });
|
|
25
74
|
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
26
|
-
|
|
75
|
+
const displayName = platform?.name || platformQuery;
|
|
76
|
+
console.log(chalk.cyan(`\nš Log into ${displayName} in the browser window.`));
|
|
27
77
|
console.log(chalk.gray(' Press Enter here when done.\n'));
|
|
28
78
|
await new Promise(res => process.stdin.once('data', () => res()));
|
|
29
|
-
await saveSession(
|
|
30
|
-
console.log(chalk.green(`ā
Session saved for ${
|
|
79
|
+
await saveSession(platformQuery, context);
|
|
80
|
+
console.log(chalk.green(`ā
Session saved for ${displayName}`));
|
|
31
81
|
await browser.close();
|
|
32
82
|
});
|
|
33
83
|
program
|
|
@@ -351,6 +401,151 @@ program
|
|
|
351
401
|
const title = await page.title();
|
|
352
402
|
console.log(JSON.stringify({ ok: true, platform, url: page.url(), title }));
|
|
353
403
|
});
|
|
404
|
+
// āāā X / Social Interactions āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
405
|
+
program
|
|
406
|
+
.command('like')
|
|
407
|
+
.description('Like the Nth post on the current page')
|
|
408
|
+
.option('--nth <n>', 'Which post (0-indexed)', '0')
|
|
409
|
+
.option('--platform <platform>', 'Platform for session', 'x')
|
|
410
|
+
.action(async (opts) => {
|
|
411
|
+
const { ensureBrowser, humanDelay } = await import('./browser.js');
|
|
412
|
+
const { browser, page } = await ensureBrowser({ platform: opts.platform });
|
|
413
|
+
try {
|
|
414
|
+
await page.goto('https://x.com/home', { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
415
|
+
await page.waitForSelector("article[data-testid='tweet']", { timeout: 20000 });
|
|
416
|
+
await humanDelay(1000, 1600);
|
|
417
|
+
await page.locator("[data-testid='like']").nth(parseInt(opts.nth)).click({ force: true });
|
|
418
|
+
await humanDelay(1000, 1400);
|
|
419
|
+
const isLiked = await page.locator("[data-testid='unlike']").count() > 0;
|
|
420
|
+
console.log(JSON.stringify({ ok: true, action: 'like', nth: opts.nth, confirmed: isLiked }));
|
|
421
|
+
}
|
|
422
|
+
catch (err) {
|
|
423
|
+
console.log(JSON.stringify({ ok: false, error: err.message }));
|
|
424
|
+
process.exit(1);
|
|
425
|
+
}
|
|
426
|
+
finally {
|
|
427
|
+
await browser.close();
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
program
|
|
431
|
+
.command('reply <text>')
|
|
432
|
+
.description('Reply to the Nth post on the current X feed')
|
|
433
|
+
.option('--nth <n>', 'Which post (0-indexed)', '0')
|
|
434
|
+
.option('--platform <platform>', 'Platform for session', 'x')
|
|
435
|
+
.action(async (text, opts) => {
|
|
436
|
+
const { ensureBrowser, humanDelay } = await import('./browser.js');
|
|
437
|
+
const { browser, page } = await ensureBrowser({ platform: opts.platform });
|
|
438
|
+
try {
|
|
439
|
+
await page.goto('https://x.com/home', { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
440
|
+
await page.waitForSelector("article[data-testid='tweet']", { timeout: 20000 });
|
|
441
|
+
await humanDelay(1000, 1500);
|
|
442
|
+
await page.locator("[data-testid='reply']").nth(parseInt(opts.nth)).click({ force: true });
|
|
443
|
+
await humanDelay(800, 1100);
|
|
444
|
+
await page.locator("[data-testid='tweetTextarea_0']").first().waitFor({ timeout: 8000 });
|
|
445
|
+
await page.locator("[data-testid='tweetTextarea_0']").first().click({ force: true });
|
|
446
|
+
await humanDelay(300, 500);
|
|
447
|
+
await page.keyboard.type(text, { delay: 38 });
|
|
448
|
+
await humanDelay(600, 900);
|
|
449
|
+
await page.locator("[data-testid='tweetButton']").first().click({ force: true });
|
|
450
|
+
await humanDelay(1800, 2400);
|
|
451
|
+
console.log(JSON.stringify({ ok: true, action: 'reply', nth: opts.nth, text }));
|
|
452
|
+
}
|
|
453
|
+
catch (err) {
|
|
454
|
+
console.log(JSON.stringify({ ok: false, error: err.message }));
|
|
455
|
+
process.exit(1);
|
|
456
|
+
}
|
|
457
|
+
finally {
|
|
458
|
+
await browser.close();
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
program
|
|
462
|
+
.command('repost')
|
|
463
|
+
.description('Repost (retweet) the Nth post on the current X feed')
|
|
464
|
+
.option('--nth <n>', 'Which post (0-indexed)', '0')
|
|
465
|
+
.option('--platform <platform>', 'Platform for session', 'x')
|
|
466
|
+
.action(async (opts) => {
|
|
467
|
+
const { ensureBrowser, humanDelay } = await import('./browser.js');
|
|
468
|
+
const { browser, page } = await ensureBrowser({ platform: opts.platform });
|
|
469
|
+
try {
|
|
470
|
+
await page.goto('https://x.com/home', { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
471
|
+
await page.waitForSelector("article[data-testid='tweet']", { timeout: 20000 });
|
|
472
|
+
await humanDelay(1000, 1500);
|
|
473
|
+
await page.locator("[data-testid='retweet']").nth(parseInt(opts.nth)).click({ force: true });
|
|
474
|
+
await humanDelay(500, 800);
|
|
475
|
+
await page.locator("[data-testid='retweetConfirm']").first().waitFor({ timeout: 5000 });
|
|
476
|
+
await page.locator("[data-testid='retweetConfirm']").first().click({ force: true });
|
|
477
|
+
await humanDelay(1200, 1800);
|
|
478
|
+
console.log(JSON.stringify({ ok: true, action: 'repost', nth: opts.nth }));
|
|
479
|
+
}
|
|
480
|
+
catch (err) {
|
|
481
|
+
console.log(JSON.stringify({ ok: false, error: err.message }));
|
|
482
|
+
process.exit(1);
|
|
483
|
+
}
|
|
484
|
+
finally {
|
|
485
|
+
await browser.close();
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
program
|
|
489
|
+
.command('quote <text>')
|
|
490
|
+
.description('Quote the Nth post on the current X feed with your comment')
|
|
491
|
+
.option('--nth <n>', 'Which post (0-indexed)', '0')
|
|
492
|
+
.option('--platform <platform>', 'Platform for session', 'x')
|
|
493
|
+
.action(async (text, opts) => {
|
|
494
|
+
const { ensureBrowser, humanDelay } = await import('./browser.js');
|
|
495
|
+
const { browser, page } = await ensureBrowser({ platform: opts.platform });
|
|
496
|
+
try {
|
|
497
|
+
await page.goto('https://x.com/home', { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
498
|
+
await page.waitForSelector("article[data-testid='tweet']", { timeout: 20000 });
|
|
499
|
+
await humanDelay(1000, 1500);
|
|
500
|
+
// Get tweet URL for the target post
|
|
501
|
+
const tweetUrls = await page.locator('a[href*="/status/"]').evaluateAll((els) => els.map(el => el.href).filter(h => /\/status\/\d+$/.test(h)));
|
|
502
|
+
const targetUrl = tweetUrls[parseInt(opts.nth)] ?? tweetUrls[0];
|
|
503
|
+
if (!targetUrl)
|
|
504
|
+
throw new Error('Could not find tweet URL for quoting');
|
|
505
|
+
// Navigate to compose with tweet URL appended
|
|
506
|
+
const composeUrl = `https://x.com/compose/post?text=${encodeURIComponent(text + '\n\n' + targetUrl)}`;
|
|
507
|
+
await page.goto(composeUrl, { waitUntil: 'domcontentloaded', timeout: 20000 });
|
|
508
|
+
await humanDelay(1500, 2000);
|
|
509
|
+
await page.locator("[data-testid='tweetButtonInline']").first().waitFor({ timeout: 8000 });
|
|
510
|
+
await page.locator("[data-testid='tweetButtonInline']").first().click({ force: true });
|
|
511
|
+
await humanDelay(2000, 2500);
|
|
512
|
+
console.log(JSON.stringify({ ok: true, action: 'quote', nth: opts.nth, text, quotedUrl: targetUrl }));
|
|
513
|
+
}
|
|
514
|
+
catch (err) {
|
|
515
|
+
console.log(JSON.stringify({ ok: false, error: err.message }));
|
|
516
|
+
process.exit(1);
|
|
517
|
+
}
|
|
518
|
+
finally {
|
|
519
|
+
await browser.close();
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
program
|
|
523
|
+
.command('post <text>')
|
|
524
|
+
.description('Post a tweet on X')
|
|
525
|
+
.option('--platform <platform>', 'Platform for session', 'x')
|
|
526
|
+
.action(async (text, opts) => {
|
|
527
|
+
const { ensureBrowser, humanDelay } = await import('./browser.js');
|
|
528
|
+
const { browser, page } = await ensureBrowser({ platform: opts.platform });
|
|
529
|
+
try {
|
|
530
|
+
await page.goto('https://x.com/home', { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
531
|
+
await page.waitForSelector("[data-testid='primaryColumn']", { timeout: 20000 });
|
|
532
|
+
await humanDelay(800, 1400);
|
|
533
|
+
await page.locator("[data-testid='tweetTextarea_0']").first().click({ force: true });
|
|
534
|
+
await humanDelay(400, 700);
|
|
535
|
+
await page.keyboard.type(text, { delay: 38 });
|
|
536
|
+
await humanDelay(600, 1000);
|
|
537
|
+
await page.locator("[data-testid='tweetButtonInline']").first().click({ force: true });
|
|
538
|
+
await humanDelay(2000, 2500);
|
|
539
|
+
console.log(JSON.stringify({ ok: true, action: 'post', text }));
|
|
540
|
+
}
|
|
541
|
+
catch (err) {
|
|
542
|
+
console.log(JSON.stringify({ ok: false, error: err.message }));
|
|
543
|
+
process.exit(1);
|
|
544
|
+
}
|
|
545
|
+
finally {
|
|
546
|
+
await browser.close();
|
|
547
|
+
}
|
|
548
|
+
});
|
|
354
549
|
program
|
|
355
550
|
.command('close')
|
|
356
551
|
.description('Close the current browser session')
|
|
@@ -368,7 +563,7 @@ program
|
|
|
368
563
|
const { isFlareSolverrUp } = await import('./local-captcha.js');
|
|
369
564
|
const sessions = await listSessions();
|
|
370
565
|
const flare = await isFlareSolverrUp();
|
|
371
|
-
console.log(chalk.cyan('\nš¶ļø veil v0.
|
|
566
|
+
console.log(chalk.cyan('\nš¶ļø veil v0.3.0 ā OpenClaw Browser Remote\n'));
|
|
372
567
|
console.log(` Sessions: ${sessions.length > 0 ? chalk.green(sessions.join(', ')) : chalk.gray('none')}`);
|
|
373
568
|
console.log(` FlareSolverr: ${flare ? chalk.green('running') : chalk.gray('not running (auto-starts on use)')}`);
|
|
374
569
|
console.log('');
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Veil Platform Directory
|
|
3
|
+
* Comprehensive mapping of platforms with login URLs, selectors, and metadata
|
|
4
|
+
*/
|
|
5
|
+
export interface PlatformConfig {
|
|
6
|
+
name: string;
|
|
7
|
+
loginUrl: string;
|
|
8
|
+
aliases: string[];
|
|
9
|
+
category: 'social' | 'ai' | 'productivity' | 'email' | 'shopping' | 'dev' | 'finance' | 'other';
|
|
10
|
+
selectors?: {
|
|
11
|
+
emailInput?: string;
|
|
12
|
+
usernameInput?: string;
|
|
13
|
+
passwordInput?: string;
|
|
14
|
+
submitButton?: string;
|
|
15
|
+
nextButton?: string;
|
|
16
|
+
verificationInput?: string;
|
|
17
|
+
};
|
|
18
|
+
postLoginCheck?: string;
|
|
19
|
+
cookies?: string[];
|
|
20
|
+
notes?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare const PLATFORMS: Record<string, PlatformConfig>;
|
|
23
|
+
export declare function getPlatform(query: string): PlatformConfig | null;
|
|
24
|
+
export declare function listPlatforms(category?: PlatformConfig['category']): PlatformConfig[];
|
|
25
|
+
export declare function searchPlatforms(query: string): PlatformConfig[];
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Veil Platform Directory
|
|
3
|
+
* Comprehensive mapping of platforms with login URLs, selectors, and metadata
|
|
4
|
+
*/
|
|
5
|
+
export const PLATFORMS = {
|
|
6
|
+
// āāā AI & Image Generation āāā
|
|
7
|
+
gemini: {
|
|
8
|
+
name: 'Google Gemini',
|
|
9
|
+
loginUrl: 'https://gemini.google.com/',
|
|
10
|
+
aliases: ['gemini', 'google-gemini', 'bard'],
|
|
11
|
+
category: 'ai',
|
|
12
|
+
postLoginCheck: '[aria-label*="Send a message"]',
|
|
13
|
+
cookies: ['__Secure-GSID', '__Secure-HSID'],
|
|
14
|
+
notes: 'Google account required',
|
|
15
|
+
},
|
|
16
|
+
'dalle-web': {
|
|
17
|
+
name: 'DALL-E (OpenAI Web)',
|
|
18
|
+
loginUrl: 'https://openai.com/api/auth/callback/auth0',
|
|
19
|
+
aliases: ['dalle', 'dalle-3', 'dall-e', 'openai-images'],
|
|
20
|
+
category: 'ai',
|
|
21
|
+
postLoginCheck: 'button[aria-label*="Generate"]',
|
|
22
|
+
cookies: ['_U'],
|
|
23
|
+
notes: 'OpenAI account required',
|
|
24
|
+
},
|
|
25
|
+
'claude-web': {
|
|
26
|
+
name: 'Claude (Anthropic Web)',
|
|
27
|
+
loginUrl: 'https://claude.ai/',
|
|
28
|
+
aliases: ['claude', 'anthropic', 'claude-web'],
|
|
29
|
+
category: 'ai',
|
|
30
|
+
postLoginCheck: 'textarea[placeholder*="Send a message"]',
|
|
31
|
+
cookies: ['__Secure-SSID'],
|
|
32
|
+
notes: 'Anthropic/Claude subscription required',
|
|
33
|
+
},
|
|
34
|
+
midjourney: {
|
|
35
|
+
name: 'Midjourney',
|
|
36
|
+
loginUrl: 'https://www.midjourney.com/auth/login',
|
|
37
|
+
aliases: ['midjourney', 'mj'],
|
|
38
|
+
category: 'ai',
|
|
39
|
+
selectors: {
|
|
40
|
+
emailInput: 'input[type="email"]',
|
|
41
|
+
passwordInput: 'input[type="password"]',
|
|
42
|
+
submitButton: 'button[type="submit"]',
|
|
43
|
+
},
|
|
44
|
+
postLoginCheck: 'div[data-testid="sidebar"]',
|
|
45
|
+
notes: 'Discord-based, requires account',
|
|
46
|
+
},
|
|
47
|
+
'stability-ai': {
|
|
48
|
+
name: 'Stability AI (DreamStudio)',
|
|
49
|
+
loginUrl: 'https://dreamstudio.ai/generate',
|
|
50
|
+
aliases: ['stability', 'dreamstudio', 'stable-diffusion-web'],
|
|
51
|
+
category: 'ai',
|
|
52
|
+
postLoginCheck: 'button:has-text("Generate")',
|
|
53
|
+
notes: 'API key or account required',
|
|
54
|
+
},
|
|
55
|
+
// āāā Social Media āāā
|
|
56
|
+
x: {
|
|
57
|
+
name: 'X (Twitter)',
|
|
58
|
+
loginUrl: 'https://x.com/login',
|
|
59
|
+
aliases: ['x', 'twitter', 'x-com'],
|
|
60
|
+
category: 'social',
|
|
61
|
+
selectors: {
|
|
62
|
+
emailInput: 'input[autocomplete="username"]',
|
|
63
|
+
passwordInput: 'input[type="password"]',
|
|
64
|
+
submitButton: 'button[type="submit"]',
|
|
65
|
+
},
|
|
66
|
+
postLoginCheck: 'a[href="/home"]',
|
|
67
|
+
cookies: ['auth_token'],
|
|
68
|
+
},
|
|
69
|
+
bluesky: {
|
|
70
|
+
name: 'Bluesky',
|
|
71
|
+
loginUrl: 'https://bsky.app/login',
|
|
72
|
+
aliases: ['bluesky', 'bsky'],
|
|
73
|
+
category: 'social',
|
|
74
|
+
selectors: {
|
|
75
|
+
usernameInput: 'input[placeholder*="Username"]',
|
|
76
|
+
passwordInput: 'input[type="password"]',
|
|
77
|
+
submitButton: 'button:has-text("Sign in")',
|
|
78
|
+
},
|
|
79
|
+
postLoginCheck: 'div[aria-label="Home timeline"]',
|
|
80
|
+
},
|
|
81
|
+
reddit: {
|
|
82
|
+
name: 'Reddit',
|
|
83
|
+
loginUrl: 'https://www.reddit.com/login',
|
|
84
|
+
aliases: ['reddit'],
|
|
85
|
+
category: 'social',
|
|
86
|
+
selectors: {
|
|
87
|
+
emailInput: 'input[id*="login-username"]',
|
|
88
|
+
passwordInput: 'input[id*="login-password"]',
|
|
89
|
+
submitButton: 'button[type="submit"]',
|
|
90
|
+
},
|
|
91
|
+
postLoginCheck: 'button[aria-label*="Create post"]',
|
|
92
|
+
cookies: ['session_tracker'],
|
|
93
|
+
},
|
|
94
|
+
linkedin: {
|
|
95
|
+
name: 'LinkedIn',
|
|
96
|
+
loginUrl: 'https://www.linkedin.com/login',
|
|
97
|
+
aliases: ['linkedin'],
|
|
98
|
+
category: 'social',
|
|
99
|
+
selectors: {
|
|
100
|
+
emailInput: 'input#username',
|
|
101
|
+
passwordInput: 'input#password',
|
|
102
|
+
submitButton: 'button[type="submit"]',
|
|
103
|
+
},
|
|
104
|
+
postLoginCheck: 'a[data-control-name="feed_home_button"]',
|
|
105
|
+
cookies: ['li_at'],
|
|
106
|
+
},
|
|
107
|
+
github: {
|
|
108
|
+
name: 'GitHub',
|
|
109
|
+
loginUrl: 'https://github.com/login',
|
|
110
|
+
aliases: ['github', 'gh'],
|
|
111
|
+
category: 'dev',
|
|
112
|
+
selectors: {
|
|
113
|
+
emailInput: 'input#login_field',
|
|
114
|
+
passwordInput: 'input#password',
|
|
115
|
+
submitButton: 'input[type="submit"]',
|
|
116
|
+
},
|
|
117
|
+
postLoginCheck: 'div[data-nav-core-github-home-feed]',
|
|
118
|
+
cookies: ['user_session', 'logged_in'],
|
|
119
|
+
},
|
|
120
|
+
// āāā Email & Productivity āāā
|
|
121
|
+
gmail: {
|
|
122
|
+
name: 'Gmail',
|
|
123
|
+
loginUrl: 'https://accounts.google.com/ServiceLogin?service=mail',
|
|
124
|
+
aliases: ['gmail', 'google-mail'],
|
|
125
|
+
category: 'email',
|
|
126
|
+
postLoginCheck: 'div[role="button"]:has-text("Compose")',
|
|
127
|
+
cookies: ['__Secure-GSID'],
|
|
128
|
+
notes: 'Google account',
|
|
129
|
+
},
|
|
130
|
+
notion: {
|
|
131
|
+
name: 'Notion',
|
|
132
|
+
loginUrl: 'https://www.notion.so/login',
|
|
133
|
+
aliases: ['notion'],
|
|
134
|
+
category: 'productivity',
|
|
135
|
+
selectors: {
|
|
136
|
+
emailInput: 'input[type="email"]',
|
|
137
|
+
submitButton: 'button:has-text("Continue with email")',
|
|
138
|
+
},
|
|
139
|
+
postLoginCheck: 'div[data-testid="sidebar"]',
|
|
140
|
+
},
|
|
141
|
+
notion_sso: {
|
|
142
|
+
name: 'Notion (Google SSO)',
|
|
143
|
+
loginUrl: 'https://www.notion.so/login?google=true',
|
|
144
|
+
aliases: ['notion-google', 'notion-sso'],
|
|
145
|
+
category: 'productivity',
|
|
146
|
+
postLoginCheck: 'div[data-testid="sidebar"]',
|
|
147
|
+
},
|
|
148
|
+
// āāā Development & APIs āāā
|
|
149
|
+
openai_platform: {
|
|
150
|
+
name: 'OpenAI Platform',
|
|
151
|
+
loginUrl: 'https://platform.openai.com/login',
|
|
152
|
+
aliases: ['openai', 'openai-api', 'openai-platform'],
|
|
153
|
+
category: 'dev',
|
|
154
|
+
selectors: {
|
|
155
|
+
emailInput: 'input[type="email"]',
|
|
156
|
+
submitButton: 'button:has-text("Continue")',
|
|
157
|
+
},
|
|
158
|
+
postLoginCheck: 'button[aria-label="Create new secret key"]',
|
|
159
|
+
cookies: ['_auth0sso'],
|
|
160
|
+
},
|
|
161
|
+
anthropic_console: {
|
|
162
|
+
name: 'Anthropic Console',
|
|
163
|
+
loginUrl: 'https://console.anthropic.com/login',
|
|
164
|
+
aliases: ['anthropic', 'anthropic-api', 'anthropic-console'],
|
|
165
|
+
category: 'dev',
|
|
166
|
+
postLoginCheck: 'button:has-text("Create API key")',
|
|
167
|
+
},
|
|
168
|
+
huggingface: {
|
|
169
|
+
name: 'Hugging Face',
|
|
170
|
+
loginUrl: 'https://huggingface.co/login',
|
|
171
|
+
aliases: ['huggingface', 'hf'],
|
|
172
|
+
category: 'dev',
|
|
173
|
+
selectors: {
|
|
174
|
+
usernameInput: 'input[name="username"]',
|
|
175
|
+
passwordInput: 'input[name="password"]',
|
|
176
|
+
submitButton: 'button[type="submit"]',
|
|
177
|
+
},
|
|
178
|
+
postLoginCheck: 'a[href*="/settings"]',
|
|
179
|
+
},
|
|
180
|
+
// āāā Shopping āāā
|
|
181
|
+
amazon: {
|
|
182
|
+
name: 'Amazon',
|
|
183
|
+
loginUrl: 'https://www.amazon.com/ap/signin',
|
|
184
|
+
aliases: ['amazon'],
|
|
185
|
+
category: 'shopping',
|
|
186
|
+
selectors: {
|
|
187
|
+
emailInput: 'input#ap_email',
|
|
188
|
+
passwordInput: 'input#ap_password',
|
|
189
|
+
submitButton: 'input#signInSubmit',
|
|
190
|
+
},
|
|
191
|
+
postLoginCheck: 'a[data-nav-ref="nav_your_account"]',
|
|
192
|
+
},
|
|
193
|
+
ebay: {
|
|
194
|
+
name: 'eBay',
|
|
195
|
+
loginUrl: 'https://signin.ebay.com/signin/',
|
|
196
|
+
aliases: ['ebay'],
|
|
197
|
+
category: 'shopping',
|
|
198
|
+
selectors: {
|
|
199
|
+
emailInput: 'input#userid',
|
|
200
|
+
passwordInput: 'input#pass',
|
|
201
|
+
submitButton: 'button#signin-continue',
|
|
202
|
+
},
|
|
203
|
+
postLoginCheck: 'a[href*="myaccount"]',
|
|
204
|
+
},
|
|
205
|
+
// āāā Finance āāā
|
|
206
|
+
stripe: {
|
|
207
|
+
name: 'Stripe Dashboard',
|
|
208
|
+
loginUrl: 'https://dashboard.stripe.com/login',
|
|
209
|
+
aliases: ['stripe'],
|
|
210
|
+
category: 'finance',
|
|
211
|
+
selectors: {
|
|
212
|
+
emailInput: 'input[type="email"]',
|
|
213
|
+
passwordInput: 'input[type="password"]',
|
|
214
|
+
submitButton: 'button[type="submit"]',
|
|
215
|
+
},
|
|
216
|
+
postLoginCheck: 'button[aria-label*="Account menu"]',
|
|
217
|
+
},
|
|
218
|
+
// āāā Miscellaneous āāā
|
|
219
|
+
google: {
|
|
220
|
+
name: 'Google (Generic)',
|
|
221
|
+
loginUrl: 'https://accounts.google.com/',
|
|
222
|
+
aliases: ['google', 'google-account'],
|
|
223
|
+
category: 'other',
|
|
224
|
+
selectors: {
|
|
225
|
+
emailInput: 'input#identifierId',
|
|
226
|
+
submitButton: 'button#identifierNext',
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
microsoft: {
|
|
230
|
+
name: 'Microsoft Account',
|
|
231
|
+
loginUrl: 'https://login.live.com/',
|
|
232
|
+
aliases: ['microsoft', 'outlook', 'hotmail'],
|
|
233
|
+
category: 'other',
|
|
234
|
+
selectors: {
|
|
235
|
+
emailInput: 'input[type="email"]',
|
|
236
|
+
submitButton: 'input[type="submit"]',
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
apple: {
|
|
240
|
+
name: 'Apple ID',
|
|
241
|
+
loginUrl: 'https://appleid.apple.com/',
|
|
242
|
+
aliases: ['apple', 'apple-id'],
|
|
243
|
+
category: 'other',
|
|
244
|
+
selectors: {
|
|
245
|
+
emailInput: 'input#user-name',
|
|
246
|
+
passwordInput: 'input#password',
|
|
247
|
+
submitButton: 'button[type="submit"]',
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
export function getPlatform(query) {
|
|
252
|
+
const lower = query.toLowerCase();
|
|
253
|
+
// Exact match in key
|
|
254
|
+
if (PLATFORMS[lower]) {
|
|
255
|
+
return PLATFORMS[lower];
|
|
256
|
+
}
|
|
257
|
+
// Check aliases
|
|
258
|
+
for (const [, platform] of Object.entries(PLATFORMS)) {
|
|
259
|
+
if (platform.aliases.includes(lower)) {
|
|
260
|
+
return platform;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// Fuzzy match on name
|
|
264
|
+
for (const [, platform] of Object.entries(PLATFORMS)) {
|
|
265
|
+
if (platform.name.toLowerCase().includes(lower)) {
|
|
266
|
+
return platform;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
export function listPlatforms(category) {
|
|
272
|
+
if (!category) {
|
|
273
|
+
return Object.values(PLATFORMS);
|
|
274
|
+
}
|
|
275
|
+
return Object.values(PLATFORMS).filter(p => p.category === category);
|
|
276
|
+
}
|
|
277
|
+
export function searchPlatforms(query) {
|
|
278
|
+
const lower = query.toLowerCase();
|
|
279
|
+
return Object.values(PLATFORMS).filter(p => p.name.toLowerCase().includes(lower) ||
|
|
280
|
+
p.aliases.some(a => a.includes(lower)));
|
|
281
|
+
}
|