veil-browser 0.1.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 +146 -0
- package/dist/ai.d.ts +19 -0
- package/dist/ai.js +253 -0
- package/dist/browser.d.ts +12 -0
- package/dist/browser.js +46 -0
- package/dist/captcha.d.ts +27 -0
- package/dist/captcha.js +203 -0
- package/dist/commands/act.d.ts +7 -0
- package/dist/commands/act.js +107 -0
- package/dist/commands/extract.d.ts +6 -0
- package/dist/commands/extract.js +64 -0
- package/dist/commands/login.d.ts +1 -0
- package/dist/commands/login.js +63 -0
- package/dist/commands/navigate.d.ts +5 -0
- package/dist/commands/navigate.js +23 -0
- package/dist/commands/screenshot.d.ts +7 -0
- package/dist/commands/screenshot.js +27 -0
- package/dist/commands/search.d.ts +13 -0
- package/dist/commands/search.js +196 -0
- package/dist/commands/session.d.ts +2 -0
- package/dist/commands/session.js +23 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +123 -0
- package/dist/local-captcha.d.ts +17 -0
- package/dist/local-captcha.js +285 -0
- package/dist/mcp.d.ts +1 -0
- package/dist/mcp.js +232 -0
- package/dist/session.d.ts +4 -0
- package/dist/session.js +37 -0
- package/dist/startup.d.ts +5 -0
- package/dist/startup.js +9 -0
- package/dist/state.d.ts +13 -0
- package/dist/state.js +45 -0
- package/package.json +54 -0
package/dist/captcha.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
const CONFIG_FILE = join(homedir(), '.veil', 'config.json');
|
|
7
|
+
async function loadConfig() {
|
|
8
|
+
try {
|
|
9
|
+
const raw = await fs.readFile(CONFIG_FILE, 'utf-8');
|
|
10
|
+
return JSON.parse(raw);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class VeilError extends Error {
|
|
17
|
+
code;
|
|
18
|
+
screenshotPath;
|
|
19
|
+
suggestion;
|
|
20
|
+
constructor(code, message, suggestion) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.code = code;
|
|
23
|
+
this.suggestion = suggestion;
|
|
24
|
+
this.name = 'VeilError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// Detect CAPTCHA on current page
|
|
28
|
+
export async function detectCaptcha(page) {
|
|
29
|
+
return await page.evaluate(() => {
|
|
30
|
+
if (document.querySelector('iframe[src*="challenges.cloudflare.com"]'))
|
|
31
|
+
return 'turnstile';
|
|
32
|
+
if (document.querySelector('iframe[src*="recaptcha"]') || document.querySelector('.g-recaptcha'))
|
|
33
|
+
return 'recaptcha';
|
|
34
|
+
if (document.querySelector('iframe[src*="hcaptcha"]') || document.querySelector('.h-captcha'))
|
|
35
|
+
return 'hcaptcha';
|
|
36
|
+
if (document.querySelector('img[alt*="captcha" i]') || document.querySelector('input[name*="captcha" i]'))
|
|
37
|
+
return 'image';
|
|
38
|
+
return null;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
// Auto-solve via 2captcha API
|
|
42
|
+
async function solveWith2Captcha(apiKey, type, page) {
|
|
43
|
+
const siteKey = await page.evaluate((t) => {
|
|
44
|
+
if (t === 'recaptcha') {
|
|
45
|
+
const el = document.querySelector('.g-recaptcha');
|
|
46
|
+
return el?.getAttribute('data-sitekey') ?? null;
|
|
47
|
+
}
|
|
48
|
+
if (t === 'hcaptcha') {
|
|
49
|
+
const el = document.querySelector('.h-captcha');
|
|
50
|
+
return el?.getAttribute('data-sitekey') ?? null;
|
|
51
|
+
}
|
|
52
|
+
if (t === 'turnstile') {
|
|
53
|
+
const el = document.querySelector('[data-sitekey]');
|
|
54
|
+
return el?.getAttribute('data-sitekey') ?? null;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}, type);
|
|
58
|
+
if (!siteKey)
|
|
59
|
+
return null;
|
|
60
|
+
const pageUrl = page.url();
|
|
61
|
+
const taskType = type === 'recaptcha' ? 'RecaptchaV2TaskProxyless'
|
|
62
|
+
: type === 'hcaptcha' ? 'HCaptchaTaskProxyless'
|
|
63
|
+
: 'TurnstileTaskProxyless';
|
|
64
|
+
const spinner = ora(`Solving ${type} CAPTCHA via 2captcha...`).start();
|
|
65
|
+
try {
|
|
66
|
+
// Submit task
|
|
67
|
+
const submitRes = await fetch('https://api.2captcha.com/createTask', {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: { 'Content-Type': 'application/json' },
|
|
70
|
+
body: JSON.stringify({
|
|
71
|
+
clientKey: apiKey,
|
|
72
|
+
task: { type: taskType, websiteURL: pageUrl, websiteKey: siteKey },
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
const submitData = await submitRes.json();
|
|
76
|
+
if (submitData.errorId > 0)
|
|
77
|
+
throw new Error(submitData.errorDescription);
|
|
78
|
+
const taskId = submitData.taskId;
|
|
79
|
+
// Poll for result (up to 120s)
|
|
80
|
+
for (let i = 0; i < 24; i++) {
|
|
81
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
82
|
+
const pollRes = await fetch('https://api.2captcha.com/getTaskResult', {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
headers: { 'Content-Type': 'application/json' },
|
|
85
|
+
body: JSON.stringify({ clientKey: apiKey, taskId }),
|
|
86
|
+
});
|
|
87
|
+
const pollData = await pollRes.json();
|
|
88
|
+
if (pollData.status === 'ready') {
|
|
89
|
+
spinner.succeed(chalk.green('ā
CAPTCHA solved!'));
|
|
90
|
+
return pollData.solution?.gRecaptchaResponse ?? pollData.solution?.token ?? null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
spinner.fail('CAPTCHA solve timed out');
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
spinner.fail(`CAPTCHA solve failed: ${err.message}`);
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Inject solved token into page
|
|
102
|
+
async function injectCaptchaToken(page, type, token) {
|
|
103
|
+
if (type === 'recaptcha') {
|
|
104
|
+
await page.evaluate((t) => {
|
|
105
|
+
const el = document.querySelector('#g-recaptcha-response');
|
|
106
|
+
if (el)
|
|
107
|
+
el.value = t;
|
|
108
|
+
// @ts-ignore
|
|
109
|
+
if (window.___grecaptcha_cfg) {
|
|
110
|
+
// Trigger callback
|
|
111
|
+
const callbacks = Object.values(window.___grecaptcha_cfg.clients ?? {});
|
|
112
|
+
// @ts-ignore
|
|
113
|
+
callbacks.forEach((c) => c?.['']?.['']?.callback?.(t));
|
|
114
|
+
}
|
|
115
|
+
}, token);
|
|
116
|
+
}
|
|
117
|
+
else if (type === 'hcaptcha') {
|
|
118
|
+
await page.evaluate((t) => {
|
|
119
|
+
const el = document.querySelector('[name="h-captcha-response"]');
|
|
120
|
+
if (el)
|
|
121
|
+
el.value = t;
|
|
122
|
+
}, token);
|
|
123
|
+
}
|
|
124
|
+
else if (type === 'turnstile') {
|
|
125
|
+
await page.evaluate((t) => {
|
|
126
|
+
const el = document.querySelector('[name="cf-turnstile-response"]');
|
|
127
|
+
if (el)
|
|
128
|
+
el.value = t;
|
|
129
|
+
}, token);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Main CAPTCHA handler ā call this before/after any page action
|
|
133
|
+
export async function handleCaptcha(page, screenshotDir) {
|
|
134
|
+
const captchaType = await detectCaptcha(page);
|
|
135
|
+
if (!captchaType)
|
|
136
|
+
return false;
|
|
137
|
+
console.log(chalk.yellow(`\nā ļø CAPTCHA detected: ${chalk.bold(captchaType)}`));
|
|
138
|
+
const config = await loadConfig();
|
|
139
|
+
// Try auto-solve
|
|
140
|
+
if (config.captcha?.apiKey && config.captcha.provider === '2captcha') {
|
|
141
|
+
const token = await solveWith2Captcha(config.captcha.apiKey, captchaType, page);
|
|
142
|
+
if (token) {
|
|
143
|
+
await injectCaptchaToken(page, captchaType, token);
|
|
144
|
+
await page.waitForTimeout(1500);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Human fallback
|
|
149
|
+
console.log(chalk.cyan('\nš§ Human needed! Opening visible browser for CAPTCHA solve...'));
|
|
150
|
+
console.log(chalk.gray(' Solve the CAPTCHA in the browser window. Veil will continue automatically.\n'));
|
|
151
|
+
// Screenshot the failure state
|
|
152
|
+
if (screenshotDir) {
|
|
153
|
+
const path = join(screenshotDir, `captcha-${Date.now()}.png`);
|
|
154
|
+
await page.screenshot({ path }).catch(() => { });
|
|
155
|
+
console.log(chalk.gray(` Screenshot: ${path}`));
|
|
156
|
+
}
|
|
157
|
+
// Wait for human to solve (up to 5 min)
|
|
158
|
+
const startUrl = page.url();
|
|
159
|
+
await new Promise((resolve) => {
|
|
160
|
+
const check = setInterval(async () => {
|
|
161
|
+
const captchaStillThere = await detectCaptcha(page).catch(() => null);
|
|
162
|
+
if (!captchaStillThere || page.url() !== startUrl) {
|
|
163
|
+
clearInterval(check);
|
|
164
|
+
resolve();
|
|
165
|
+
}
|
|
166
|
+
}, 2000);
|
|
167
|
+
setTimeout(() => { clearInterval(check); resolve(); }, 300000);
|
|
168
|
+
});
|
|
169
|
+
console.log(chalk.green('ā
CAPTCHA cleared, resuming...\n'));
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
// Retry wrapper with backoff
|
|
173
|
+
export async function withRetry(fn, opts = {}) {
|
|
174
|
+
const { attempts = 3, delay = 2000, label = 'action' } = opts;
|
|
175
|
+
let lastError = null;
|
|
176
|
+
for (let i = 1; i <= attempts; i++) {
|
|
177
|
+
try {
|
|
178
|
+
return await fn();
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
lastError = err;
|
|
182
|
+
if (i < attempts) {
|
|
183
|
+
const wait = delay * i;
|
|
184
|
+
console.log(chalk.yellow(` ā³ Retry ${i}/${attempts} for "${label}" in ${wait}ms...`));
|
|
185
|
+
await new Promise((r) => setTimeout(r, wait));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
throw lastError;
|
|
190
|
+
}
|
|
191
|
+
// Screenshot on error helper
|
|
192
|
+
export async function screenshotOnError(page, label) {
|
|
193
|
+
const dir = join(homedir(), '.veil', 'errors');
|
|
194
|
+
await fs.mkdir(dir, { recursive: true });
|
|
195
|
+
const path = join(dir, `error-${label}-${Date.now()}.png`);
|
|
196
|
+
try {
|
|
197
|
+
await page.screenshot({ path });
|
|
198
|
+
return path;
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { getBrowser, closeBrowser, humanDelay } from '../browser.js';
|
|
4
|
+
import { aiAct } from '../ai.js';
|
|
5
|
+
// Fallback: simple heuristic actions (no LLM needed)
|
|
6
|
+
async function performFallbackAction(page, instruction) {
|
|
7
|
+
const lower = instruction.toLowerCase();
|
|
8
|
+
const clickMatch = lower.match(/^click\s+(?:on\s+)?["']?(.+?)["']?\s*$/);
|
|
9
|
+
if (clickMatch) {
|
|
10
|
+
const target = clickMatch[1];
|
|
11
|
+
await page.getByRole('button', { name: new RegExp(target, 'i') }).first().click().catch(async () => {
|
|
12
|
+
await page.getByText(new RegExp(target, 'i')).first().click();
|
|
13
|
+
});
|
|
14
|
+
await humanDelay();
|
|
15
|
+
return `Clicked: ${target}`;
|
|
16
|
+
}
|
|
17
|
+
const typeMatch = lower.match(/^type\s+["'](.+?)["']\s+(?:in|into)\s+["']?(.+?)["']?\s*$/);
|
|
18
|
+
if (typeMatch) {
|
|
19
|
+
const [, text, field] = typeMatch;
|
|
20
|
+
await page.getByRole('textbox', { name: new RegExp(field, 'i') }).fill(text);
|
|
21
|
+
await humanDelay();
|
|
22
|
+
return `Typed "${text}" into ${field}`;
|
|
23
|
+
}
|
|
24
|
+
const tweetMatch = instruction.match(/^post\s+(?:tweet|on twitter|on x)[:\s]+["']?(.+?)["']?\s*$/i);
|
|
25
|
+
if (tweetMatch) {
|
|
26
|
+
const tweetText = tweetMatch[1];
|
|
27
|
+
await page.goto('https://x.com/compose/post', { waitUntil: 'domcontentloaded' });
|
|
28
|
+
await humanDelay(1000, 2000);
|
|
29
|
+
const editor = page.locator('[data-testid="tweetTextarea_0"]');
|
|
30
|
+
await editor.click();
|
|
31
|
+
await humanDelay(300, 600);
|
|
32
|
+
await editor.fill(tweetText);
|
|
33
|
+
await humanDelay(500, 1000);
|
|
34
|
+
await page.locator('[data-testid="tweetButtonInline"]').click();
|
|
35
|
+
await humanDelay(1000, 2000);
|
|
36
|
+
return `Posted tweet: "${tweetText}"`;
|
|
37
|
+
}
|
|
38
|
+
if (lower.includes('like') && (lower.includes('tweet') || lower.includes('post'))) {
|
|
39
|
+
await page.locator('[data-testid="like"]').first().click();
|
|
40
|
+
await humanDelay();
|
|
41
|
+
return 'Liked the post';
|
|
42
|
+
}
|
|
43
|
+
if (lower.includes('scroll down')) {
|
|
44
|
+
await page.evaluate(() => window.scrollBy(0, 600));
|
|
45
|
+
await humanDelay();
|
|
46
|
+
return 'Scrolled down';
|
|
47
|
+
}
|
|
48
|
+
if (lower.includes('scroll up')) {
|
|
49
|
+
await page.evaluate(() => window.scrollBy(0, -600));
|
|
50
|
+
await humanDelay();
|
|
51
|
+
return 'Scrolled up';
|
|
52
|
+
}
|
|
53
|
+
if (lower.includes('press enter') || lower.includes('submit')) {
|
|
54
|
+
await page.keyboard.press('Enter');
|
|
55
|
+
await humanDelay();
|
|
56
|
+
return 'Pressed Enter';
|
|
57
|
+
}
|
|
58
|
+
const gotoMatch = instruction.match(/^(?:go to|navigate to|open)\s+(https?:\/\/\S+)/i);
|
|
59
|
+
if (gotoMatch) {
|
|
60
|
+
await page.goto(gotoMatch[1], { waitUntil: 'domcontentloaded' });
|
|
61
|
+
await humanDelay(800, 1500);
|
|
62
|
+
return `Navigated to ${gotoMatch[1]}`;
|
|
63
|
+
}
|
|
64
|
+
throw new Error(`Could not parse instruction without AI: "${instruction}". Set an LLM API key for smart act.`);
|
|
65
|
+
}
|
|
66
|
+
export async function actCommand(instruction, opts) {
|
|
67
|
+
const { page } = await getBrowser({ headed: opts.headed, platform: opts.platform });
|
|
68
|
+
try {
|
|
69
|
+
if (opts.url) {
|
|
70
|
+
const spinner = ora({ text: `Loading ${opts.url}...`, color: 'cyan' }).start();
|
|
71
|
+
await page.goto(opts.url, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
72
|
+
await humanDelay(800, 1500);
|
|
73
|
+
spinner.succeed(chalk.gray(`Loaded ${opts.url}`));
|
|
74
|
+
}
|
|
75
|
+
// Try AI-powered act first
|
|
76
|
+
try {
|
|
77
|
+
const result = await aiAct(page, instruction, { verbose: opts.verbose });
|
|
78
|
+
if (opts.json) {
|
|
79
|
+
console.log(JSON.stringify({ success: result.success, action: instruction, steps: result.steps }));
|
|
80
|
+
}
|
|
81
|
+
if (!result.success)
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
catch (aiErr) {
|
|
85
|
+
// If AI not configured, fall back to heuristics
|
|
86
|
+
if (aiErr.message.includes('No LLM configured')) {
|
|
87
|
+
console.log(chalk.yellow('ā ļø No LLM configured, using fallback heuristics...'));
|
|
88
|
+
const result = await performFallbackAction(page, instruction);
|
|
89
|
+
console.log(chalk.green(`ā
${result}`));
|
|
90
|
+
if (opts.json)
|
|
91
|
+
console.log(JSON.stringify({ success: true, action: instruction, result }));
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
throw aiErr;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
console.error(chalk.red(`ā Action failed: ${err.message}`));
|
|
100
|
+
if (opts.json)
|
|
101
|
+
console.log(JSON.stringify({ success: false, action: instruction, error: err.message }));
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
await closeBrowser(opts.platform);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { getBrowser, closeBrowser, humanDelay } from '../browser.js';
|
|
4
|
+
export async function extractCommand(query, opts) {
|
|
5
|
+
const spinner = ora({ text: `Extracting: ${query}`, color: 'cyan' }).start();
|
|
6
|
+
const { page } = await getBrowser({ headed: opts.headed, platform: opts.platform });
|
|
7
|
+
try {
|
|
8
|
+
if (opts.url) {
|
|
9
|
+
await page.goto(opts.url, { waitUntil: 'domcontentloaded' });
|
|
10
|
+
await humanDelay(800, 1500);
|
|
11
|
+
}
|
|
12
|
+
const lower = query.toLowerCase();
|
|
13
|
+
let result;
|
|
14
|
+
// Twitter/X feed
|
|
15
|
+
if (lower.includes('tweet') || lower.includes('feed') || lower.includes('timeline')) {
|
|
16
|
+
const tweets = await page.locator('[data-testid="tweet"]').all();
|
|
17
|
+
const data = await Promise.all(tweets.slice(0, 20).map(async (t) => {
|
|
18
|
+
const text = await t.locator('[data-testid="tweetText"]').textContent().catch(() => '');
|
|
19
|
+
const author = await t.locator('[data-testid="User-Name"]').textContent().catch(() => '');
|
|
20
|
+
return { author: author?.trim(), text: text?.trim() };
|
|
21
|
+
}));
|
|
22
|
+
result = data.filter((d) => d.text);
|
|
23
|
+
}
|
|
24
|
+
// Links
|
|
25
|
+
else if (lower.includes('link') || lower.includes('url')) {
|
|
26
|
+
const links = await page.locator('a[href]').all();
|
|
27
|
+
result = await Promise.all(links.slice(0, 50).map(async (l) => ({
|
|
28
|
+
text: (await l.textContent())?.trim(),
|
|
29
|
+
href: await l.getAttribute('href'),
|
|
30
|
+
})));
|
|
31
|
+
}
|
|
32
|
+
// Page text
|
|
33
|
+
else if (lower.includes('text') || lower.includes('content')) {
|
|
34
|
+
result = await page.evaluate(() => document.body.innerText.slice(0, 5000));
|
|
35
|
+
}
|
|
36
|
+
// Title
|
|
37
|
+
else if (lower.includes('title')) {
|
|
38
|
+
result = await page.title();
|
|
39
|
+
}
|
|
40
|
+
// Default: page metadata
|
|
41
|
+
else {
|
|
42
|
+
result = {
|
|
43
|
+
title: await page.title(),
|
|
44
|
+
url: page.url(),
|
|
45
|
+
text: await page.evaluate(() => document.body.innerText.slice(0, 2000)),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
spinner.succeed(chalk.green('ā
Extracted'));
|
|
49
|
+
if (opts.json) {
|
|
50
|
+
console.log(JSON.stringify({ success: true, query, result }, null, 2));
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
console.log(chalk.cyan('\nResult:'));
|
|
54
|
+
console.log(JSON.stringify(result, null, 2));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
spinner.fail(chalk.red(`ā Extract failed: ${err.message}`));
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
await closeBrowser(opts.platform);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function loginCommand(platform: string): Promise<void>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { getBrowser, closeBrowser } from '../browser.js';
|
|
4
|
+
import { loadSession } from '../session.js';
|
|
5
|
+
const PLATFORM_LOGIN_URLS = {
|
|
6
|
+
twitter: 'https://x.com/login',
|
|
7
|
+
x: 'https://x.com/login',
|
|
8
|
+
reddit: 'https://www.reddit.com/login',
|
|
9
|
+
linkedin: 'https://www.linkedin.com/login',
|
|
10
|
+
github: 'https://github.com/login',
|
|
11
|
+
instagram: 'https://www.instagram.com/accounts/login',
|
|
12
|
+
};
|
|
13
|
+
const PLATFORM_SUCCESS_PATTERNS = {
|
|
14
|
+
twitter: ['x.com/home', 'twitter.com/home'],
|
|
15
|
+
x: ['x.com/home', 'twitter.com/home'],
|
|
16
|
+
reddit: ['reddit.com/?'],
|
|
17
|
+
linkedin: ['linkedin.com/feed'],
|
|
18
|
+
github: ['github.com', '!/login', '!/session'],
|
|
19
|
+
instagram: ['instagram.com', '!/accounts/login'],
|
|
20
|
+
};
|
|
21
|
+
function isLoggedIn(url, platform) {
|
|
22
|
+
const patterns = PLATFORM_SUCCESS_PATTERNS[platform.toLowerCase()] ?? [];
|
|
23
|
+
return patterns.some((p) => {
|
|
24
|
+
if (p.startsWith('!'))
|
|
25
|
+
return !url.includes(p.slice(1));
|
|
26
|
+
return url.includes(p);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export async function loginCommand(platform) {
|
|
30
|
+
const p = platform.toLowerCase();
|
|
31
|
+
const loginUrl = PLATFORM_LOGIN_URLS[p] ?? `https://${p}.com/login`;
|
|
32
|
+
const existing = await loadSession(p);
|
|
33
|
+
if (existing) {
|
|
34
|
+
console.log(chalk.yellow(`ā ļø Session for ${chalk.bold(platform)} already exists. Use ${chalk.bold('veil logout ' + platform)} to clear it first.`));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
console.log(chalk.cyan(`\nš Opening browser for ${chalk.bold(platform)} login...`));
|
|
38
|
+
console.log(chalk.gray(' Complete the login in the browser window. Veil will detect when you\'re done.\n'));
|
|
39
|
+
const { browser, context, page } = await getBrowser({ headed: true, platform: p });
|
|
40
|
+
await page.goto(loginUrl);
|
|
41
|
+
const spinner = ora({ text: 'Waiting for login...', color: 'cyan' }).start();
|
|
42
|
+
await new Promise((resolve) => {
|
|
43
|
+
const check = async () => {
|
|
44
|
+
try {
|
|
45
|
+
const url = page.url();
|
|
46
|
+
if (isLoggedIn(url, p)) {
|
|
47
|
+
clearInterval(interval);
|
|
48
|
+
resolve();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch { }
|
|
52
|
+
};
|
|
53
|
+
const interval = setInterval(check, 1000);
|
|
54
|
+
// Also handle manual close
|
|
55
|
+
browser.on('disconnected', () => {
|
|
56
|
+
clearInterval(interval);
|
|
57
|
+
resolve();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
spinner.succeed(chalk.green(`ā
Logged in to ${chalk.bold(platform)}! Saving session...`));
|
|
61
|
+
await closeBrowser(p);
|
|
62
|
+
console.log(chalk.green(`\nš Session saved! You can now use ${chalk.bold('veil')} with ${chalk.bold(platform)} in headless mode.\n`));
|
|
63
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { getBrowser, closeBrowser, humanDelay } from '../browser.js';
|
|
4
|
+
export async function navigateCommand(url, opts) {
|
|
5
|
+
const spinner = ora({ text: `Navigating to ${url}...`, color: 'cyan' }).start();
|
|
6
|
+
const { page } = await getBrowser({ headed: opts.headed, platform: opts.platform });
|
|
7
|
+
try {
|
|
8
|
+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
9
|
+
await humanDelay(500, 1200);
|
|
10
|
+
const title = await page.title();
|
|
11
|
+
const finalUrl = page.url();
|
|
12
|
+
spinner.succeed(chalk.green(`Navigated to: ${chalk.bold(title)}`));
|
|
13
|
+
if (opts.json) {
|
|
14
|
+
console.log(JSON.stringify({ success: true, url: finalUrl, title }));
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
console.log(chalk.gray(` URL: ${finalUrl}`));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
await closeBrowser(opts.platform);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { getBrowser, closeBrowser, humanDelay } from '../browser.js';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
export async function screenshotCommand(opts) {
|
|
6
|
+
const outPath = opts.output ?? join(process.cwd(), `screenshot-${Date.now()}.png`);
|
|
7
|
+
const spinner = ora({ text: 'Taking screenshot...', color: 'cyan' }).start();
|
|
8
|
+
const { page } = await getBrowser({ headed: opts.headed, platform: opts.platform });
|
|
9
|
+
try {
|
|
10
|
+
if (opts.url) {
|
|
11
|
+
await page.goto(opts.url, { waitUntil: 'domcontentloaded' });
|
|
12
|
+
await humanDelay(800, 1500);
|
|
13
|
+
}
|
|
14
|
+
await page.screenshot({ path: outPath, fullPage: false });
|
|
15
|
+
spinner.succeed(chalk.green(`šø Screenshot saved: ${chalk.bold(outPath)}`));
|
|
16
|
+
if (opts.json) {
|
|
17
|
+
console.log(JSON.stringify({ success: true, path: outPath }));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
spinner.fail(chalk.red(`ā Screenshot failed: ${err.message}`));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
await closeBrowser(opts.platform);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface SearchResult {
|
|
2
|
+
title: string;
|
|
3
|
+
url: string;
|
|
4
|
+
snippet: string;
|
|
5
|
+
source: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function searchCommand(query: string, opts: {
|
|
8
|
+
engine?: string;
|
|
9
|
+
platform?: string;
|
|
10
|
+
headed?: boolean;
|
|
11
|
+
json?: boolean;
|
|
12
|
+
limit?: string;
|
|
13
|
+
}): Promise<void>;
|