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/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.2.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 (x, linkedin, reddit)')
15
- .action(async (platform) => {
16
- const platformUrls = {
17
- x: 'https://x.com/login',
18
- twitter: 'https://x.com/login',
19
- linkedin: 'https://www.linkedin.com/login',
20
- reddit: 'https://www.reddit.com/login',
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
- console.log(chalk.cyan(`\nšŸ” Log into ${platform} in the browser window.`));
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(platform, context);
30
- console.log(chalk.green(`āœ… Session saved for ${platform}`));
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.2.0 — OpenClaw Browser Remote\n'));
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veil-browser",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "Stealth browser CLI for AI agents — bypass bot detection, persist sessions, local CAPTCHA solving, MCP server",
5
5
  "keywords": [
6
6
  "browser",