stealth-cli 0.5.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.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +295 -0
  3. package/bin/stealth.js +50 -0
  4. package/package.json +65 -0
  5. package/skills/SKILL.md +244 -0
  6. package/src/browser.js +341 -0
  7. package/src/client.js +115 -0
  8. package/src/commands/batch.js +180 -0
  9. package/src/commands/browse.js +101 -0
  10. package/src/commands/config.js +85 -0
  11. package/src/commands/crawl.js +169 -0
  12. package/src/commands/daemon.js +143 -0
  13. package/src/commands/extract.js +153 -0
  14. package/src/commands/fingerprint.js +306 -0
  15. package/src/commands/interactive.js +284 -0
  16. package/src/commands/mcp.js +68 -0
  17. package/src/commands/monitor.js +160 -0
  18. package/src/commands/pdf.js +109 -0
  19. package/src/commands/profile.js +112 -0
  20. package/src/commands/proxy.js +116 -0
  21. package/src/commands/screenshot.js +96 -0
  22. package/src/commands/search.js +162 -0
  23. package/src/commands/serve.js +240 -0
  24. package/src/config.js +123 -0
  25. package/src/cookies.js +67 -0
  26. package/src/daemon-entry.js +19 -0
  27. package/src/daemon.js +294 -0
  28. package/src/errors.js +136 -0
  29. package/src/extractors/base.js +59 -0
  30. package/src/extractors/bing.js +47 -0
  31. package/src/extractors/duckduckgo.js +91 -0
  32. package/src/extractors/github.js +103 -0
  33. package/src/extractors/google.js +173 -0
  34. package/src/extractors/index.js +55 -0
  35. package/src/extractors/youtube.js +87 -0
  36. package/src/humanize.js +210 -0
  37. package/src/index.js +32 -0
  38. package/src/macros.js +36 -0
  39. package/src/mcp-server.js +341 -0
  40. package/src/output.js +65 -0
  41. package/src/profiles.js +308 -0
  42. package/src/proxy-pool.js +256 -0
  43. package/src/retry.js +112 -0
  44. package/src/session.js +159 -0
@@ -0,0 +1,160 @@
1
+ /**
2
+ * stealth monitor <url> - Monitor a page for changes
3
+ */
4
+
5
+ import ora from 'ora';
6
+ import chalk from 'chalk';
7
+ import { launchBrowser, closeBrowser, navigate, evaluate, waitForReady } from '../browser.js';
8
+ import { randomDelay } from '../humanize.js';
9
+ import { log } from '../output.js';
10
+
11
+ export function registerMonitor(program) {
12
+ program
13
+ .command('monitor')
14
+ .description('Monitor a page for changes')
15
+ .argument('<url>', 'URL to monitor')
16
+ .option('-s, --selector <selector>', 'CSS selector to watch (default: body)')
17
+ .option('-i, --interval <seconds>', 'Check interval in seconds', '60')
18
+ .option('-n, --count <n>', 'Max checks (0 = infinite)', '0')
19
+ .option('--attr <attribute>', 'Watch an attribute instead of text')
20
+ .option('--contains <text>', 'Alert when page contains this text')
21
+ .option('--not-contains <text>', 'Alert when page no longer contains this text')
22
+ .option('--json', 'Output changes as JSON')
23
+ .option('--proxy <proxy>', 'Proxy server')
24
+ .option('--profile <name>', 'Use a browser profile')
25
+ .option('--proxy-rotate', 'Rotate proxy from pool')
26
+ .option('--no-headless', 'Show browser window')
27
+ .action(async (url, opts) => {
28
+ const interval = parseInt(opts.interval) * 1000;
29
+ const maxChecks = parseInt(opts.count);
30
+ const selector = opts.selector || 'body';
31
+
32
+ log.info(`Monitoring ${url}`);
33
+ log.dim(` Selector: ${selector}`);
34
+ log.dim(` Interval: ${opts.interval}s`);
35
+ if (maxChecks > 0) log.dim(` Max checks: ${maxChecks}`);
36
+ console.log();
37
+
38
+ let handle;
39
+ let previousValue = null;
40
+ let checkCount = 0;
41
+ let changeCount = 0;
42
+
43
+ try {
44
+ handle = await launchBrowser({
45
+ headless: opts.headless,
46
+ proxy: opts.proxy,
47
+ proxyRotate: opts.proxyRotate,
48
+ profile: opts.profile,
49
+ });
50
+
51
+ if (handle.isDaemon) {
52
+ log.error('Monitor requires direct mode');
53
+ process.exit(1);
54
+ }
55
+
56
+ // Main monitoring loop
57
+ while (true) {
58
+ checkCount++;
59
+ const ts = new Date().toISOString();
60
+
61
+ try {
62
+ await navigate(handle, url, { retries: 1 });
63
+ await waitForReady(handle.page, { timeout: 5000 });
64
+
65
+ // Extract current value
66
+ let currentValue;
67
+ if (opts.attr) {
68
+ currentValue = await handle.page.$eval(selector, (el, attr) => el.getAttribute(attr), opts.attr)
69
+ .catch(() => null);
70
+ } else {
71
+ currentValue = await handle.page.$eval(selector, (el) => el.textContent?.trim())
72
+ .catch(() => null);
73
+ }
74
+
75
+ // Text contains check
76
+ if (opts.contains) {
77
+ const found = currentValue?.includes(opts.contains);
78
+ if (found) {
79
+ changeCount++;
80
+ const msg = `[${ts}] ✓ FOUND: "${opts.contains}"`;
81
+ if (opts.json) {
82
+ console.log(JSON.stringify({ event: 'found', text: opts.contains, url, timestamp: ts, check: checkCount }));
83
+ } else {
84
+ console.log(chalk.green(msg));
85
+ }
86
+ } else {
87
+ if (!opts.json) process.stderr.write(chalk.dim(`[${ts}] Check #${checkCount} — not found\r`));
88
+ }
89
+ }
90
+ // Text not-contains check
91
+ else if (opts.notContains) {
92
+ const found = currentValue?.includes(opts.notContains);
93
+ if (!found) {
94
+ changeCount++;
95
+ const msg = `[${ts}] ✓ DISAPPEARED: "${opts.notContains}"`;
96
+ if (opts.json) {
97
+ console.log(JSON.stringify({ event: 'disappeared', text: opts.notContains, url, timestamp: ts, check: checkCount }));
98
+ } else {
99
+ console.log(chalk.yellow(msg));
100
+ }
101
+ } else {
102
+ if (!opts.json) process.stderr.write(chalk.dim(`[${ts}] Check #${checkCount} — still present\r`));
103
+ }
104
+ }
105
+ // Diff check
106
+ else {
107
+ if (previousValue === null) {
108
+ previousValue = currentValue;
109
+ if (!opts.json) {
110
+ log.info(`[${ts}] Initial snapshot captured (${currentValue?.length || 0} chars)`);
111
+ }
112
+ } else if (currentValue !== previousValue) {
113
+ changeCount++;
114
+
115
+ if (opts.json) {
116
+ console.log(JSON.stringify({
117
+ event: 'changed',
118
+ url,
119
+ selector,
120
+ previous: previousValue?.slice(0, 500),
121
+ current: currentValue?.slice(0, 500),
122
+ timestamp: ts,
123
+ check: checkCount,
124
+ changeNumber: changeCount,
125
+ }));
126
+ } else {
127
+ console.log(chalk.yellow(`\n[${ts}] CHANGE #${changeCount} detected!`));
128
+ console.log(chalk.dim(' Previous:'), (previousValue || '').slice(0, 200));
129
+ console.log(chalk.cyan(' Current: '), (currentValue || '').slice(0, 200));
130
+ console.log();
131
+ }
132
+
133
+ previousValue = currentValue;
134
+ } else {
135
+ if (!opts.json) process.stderr.write(chalk.dim(`[${ts}] Check #${checkCount} — no change\r`));
136
+ }
137
+ }
138
+ } catch (err) {
139
+ log.warn(`Check #${checkCount} failed: ${err.message}`);
140
+ }
141
+
142
+ // Check if we should stop
143
+ if (maxChecks > 0 && checkCount >= maxChecks) {
144
+ break;
145
+ }
146
+
147
+ // Wait for next check (with jitter)
148
+ await randomDelay(interval * 0.9, interval * 1.1);
149
+ }
150
+
151
+ console.log();
152
+ log.success(`Monitoring complete: ${checkCount} checks, ${changeCount} changes`);
153
+ } catch (err) {
154
+ log.error(`Monitor failed: ${err.message}`);
155
+ process.exit(1);
156
+ } finally {
157
+ if (handle) await closeBrowser(handle);
158
+ }
159
+ });
160
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * stealth pdf <url> - Save a page as PDF
3
+ */
4
+
5
+ import ora from 'ora';
6
+ import { launchBrowser, closeBrowser, navigate, waitForReady } from '../browser.js';
7
+ import { log } from '../output.js';
8
+
9
+ export function registerPdf(program) {
10
+ program
11
+ .command('pdf')
12
+ .description('Save a page as PDF')
13
+ .argument('<url>', 'URL to convert')
14
+ .option('-o, --output <file>', 'Output file path', 'page.pdf')
15
+ .option('--format <size>', 'Paper size: A4, Letter, Legal, A3', 'A4')
16
+ .option('--landscape', 'Landscape orientation')
17
+ .option('--margin <px>', 'Margin in pixels (all sides)', '20')
18
+ .option('--no-background', 'Omit background graphics')
19
+ .option('--scale <n>', 'Scale factor (0.1-2.0)', '1')
20
+ .option('--header <text>', 'Header template (HTML)')
21
+ .option('--footer <text>', 'Footer template (HTML)')
22
+ .option('--pages <range>', 'Page range (e.g. "1-3", "1,3,5")')
23
+ .option('--wait <ms>', 'Wait time after page load (ms)', '2000')
24
+ .option('--proxy <proxy>', 'Proxy server')
25
+ .option('--cookies <file>', 'Load cookies from Netscape-format file')
26
+ .option('--profile <name>', 'Use a browser profile')
27
+ .option('--no-headless', 'Show browser window')
28
+ .option('--retries <n>', 'Max retries on failure', '2')
29
+ .action(async (url, opts) => {
30
+ const spinner = ora('Launching stealth browser...').start();
31
+ let handle;
32
+
33
+ try {
34
+ handle = await launchBrowser({
35
+ headless: opts.headless,
36
+ proxy: opts.proxy,
37
+ profile: opts.profile,
38
+ });
39
+
40
+ if (handle.isDaemon) {
41
+ spinner.stop();
42
+ log.error('PDF generation requires direct mode (daemon does not support it)');
43
+ log.dim(' Tip: stop daemon first or use --profile to force direct mode');
44
+ process.exit(1);
45
+ }
46
+
47
+ if (opts.cookies) {
48
+ const { loadCookies } = await import('../cookies.js');
49
+ await loadCookies(handle.context, opts.cookies);
50
+ }
51
+
52
+ spinner.text = `Navigating to ${url}...`;
53
+ await navigate(handle, url, { retries: parseInt(opts.retries) });
54
+ await waitForReady(handle.page, { timeout: parseInt(opts.wait) });
55
+
56
+ spinner.text = 'Generating PDF...';
57
+
58
+ // Firefox/Camoufox doesn't support page.pdf() — use print-to-PDF via CDP
59
+ // Fallback: take a full-page screenshot and convert context
60
+ try {
61
+ await handle.page.pdf({
62
+ path: opts.output,
63
+ format: opts.format,
64
+ landscape: opts.landscape || false,
65
+ printBackground: opts.background !== false,
66
+ scale: parseFloat(opts.scale),
67
+ margin: { top: `${opts.margin}px`, right: `${opts.margin}px`, bottom: `${opts.margin}px`, left: `${opts.margin}px` },
68
+ });
69
+ } catch {
70
+ // Firefox fallback: emulate print media and take full screenshot
71
+ // This creates a high-quality image, not a true PDF
72
+ spinner.text = 'Firefox detected — using screenshot-based PDF...';
73
+
74
+ const outputFile = opts.output.endsWith('.pdf')
75
+ ? opts.output.replace(/\.pdf$/, '.png')
76
+ : opts.output + '.png';
77
+
78
+ await handle.page.screenshot({
79
+ path: outputFile,
80
+ fullPage: true,
81
+ type: 'png',
82
+ });
83
+
84
+ log.warn('Firefox/Camoufox does not support native PDF generation');
85
+ log.dim(` Full-page screenshot saved instead: ${outputFile}`);
86
+ log.dim(' Tip: Use a tool like "img2pdf" to convert: img2pdf screenshot.png -o page.pdf');
87
+
88
+ spinner.stop();
89
+ return;
90
+ }
91
+
92
+ spinner.stop();
93
+
94
+ const { statSync } = await import('fs');
95
+ const size = statSync(opts.output).size;
96
+ const sizeKB = (size / 1024).toFixed(1);
97
+
98
+ log.success(`PDF saved: ${opts.output} (${sizeKB} KB)`);
99
+ log.dim(` URL: ${handle.page.url()}`);
100
+ log.dim(` Format: ${opts.format}${opts.landscape ? ' landscape' : ''}`);
101
+ } catch (err) {
102
+ spinner.stop();
103
+ log.error(`PDF failed: ${err.message}`);
104
+ process.exit(1);
105
+ } finally {
106
+ if (handle) await closeBrowser(handle);
107
+ }
108
+ });
109
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * stealth profile - Manage browser identity profiles
3
+ */
4
+
5
+ import {
6
+ createProfile, loadProfile, deleteProfile,
7
+ listProfiles, getPresets,
8
+ } from '../profiles.js';
9
+ import { log } from '../output.js';
10
+ import chalk from 'chalk';
11
+
12
+ export function registerProfile(program) {
13
+ const profile = program
14
+ .command('profile')
15
+ .description('Manage browser identity profiles');
16
+
17
+ // stealth profile create <name>
18
+ profile
19
+ .command('create')
20
+ .description('Create a new profile')
21
+ .argument('<name>', 'Profile name')
22
+ .option('--preset <preset>', `Use a preset: ${getPresets().join(', ')}`)
23
+ .option('--random', 'Generate random fingerprint')
24
+ .option('--locale <locale>', 'Browser locale (e.g. en-US, zh-CN)')
25
+ .option('--timezone <tz>', 'Timezone (e.g. America/New_York)')
26
+ .option('--proxy <proxy>', 'Proxy server for this profile')
27
+ .option('--os <os>', 'OS fingerprint: windows, macos, linux')
28
+ .action((name, opts) => {
29
+ try {
30
+ const p = createProfile(name, opts);
31
+ log.success(`Profile "${name}" created`);
32
+ log.dim(` Locale: ${p.fingerprint.locale}`);
33
+ log.dim(` Timezone: ${p.fingerprint.timezone}`);
34
+ log.dim(` OS: ${p.fingerprint.os}`);
35
+ log.dim(` Viewport: ${p.fingerprint.viewport.width}x${p.fingerprint.viewport.height}`);
36
+ if (p.proxy) log.dim(` Proxy: ${p.proxy}`);
37
+ } catch (err) {
38
+ log.error(err.message);
39
+ process.exit(1);
40
+ }
41
+ });
42
+
43
+ // stealth profile list
44
+ profile
45
+ .command('list')
46
+ .description('List all profiles')
47
+ .action(() => {
48
+ const profiles = listProfiles();
49
+ if (profiles.length === 0) {
50
+ log.info('No profiles yet. Create one with: stealth profile create <name>');
51
+ return;
52
+ }
53
+
54
+ console.log(chalk.bold('\n Profiles:\n'));
55
+ const header = ` ${'Name'.padEnd(18)} ${'Locale'.padEnd(8)} ${'OS'.padEnd(8)} ${'Viewport'.padEnd(12)} ${'Proxy'.padEnd(6)} ${'Cookies'.padEnd(8)} ${'Uses'.padEnd(6)}`;
56
+ console.log(chalk.dim(header));
57
+ console.log(chalk.dim(' ' + '─'.repeat(70)));
58
+
59
+ for (const p of profiles) {
60
+ if (p.error) {
61
+ console.log(` ${chalk.red(p.name.padEnd(18))} ${chalk.dim('corrupted')}`);
62
+ continue;
63
+ }
64
+ console.log(
65
+ ` ${chalk.cyan(p.name.padEnd(18))} ${p.locale.padEnd(8)} ${p.os.padEnd(8)} ${p.viewport.padEnd(12)} ${p.proxy.padEnd(6)} ${String(p.cookies).padEnd(8)} ${String(p.useCount).padEnd(6)}`,
66
+ );
67
+ }
68
+ console.log();
69
+ });
70
+
71
+ // stealth profile show <name>
72
+ profile
73
+ .command('show')
74
+ .description('Show profile details')
75
+ .argument('<name>', 'Profile name')
76
+ .action((name) => {
77
+ try {
78
+ const p = loadProfile(name);
79
+ console.log(JSON.stringify(p, null, 2));
80
+ } catch (err) {
81
+ log.error(err.message);
82
+ process.exit(1);
83
+ }
84
+ });
85
+
86
+ // stealth profile delete <name>
87
+ profile
88
+ .command('delete')
89
+ .description('Delete a profile')
90
+ .argument('<name>', 'Profile name')
91
+ .action((name) => {
92
+ try {
93
+ deleteProfile(name);
94
+ log.success(`Profile "${name}" deleted`);
95
+ } catch (err) {
96
+ log.error(err.message);
97
+ process.exit(1);
98
+ }
99
+ });
100
+
101
+ // stealth profile presets
102
+ profile
103
+ .command('presets')
104
+ .description('List available fingerprint presets')
105
+ .action(() => {
106
+ console.log(chalk.bold('\n Available presets:\n'));
107
+ for (const name of getPresets()) {
108
+ console.log(` ${chalk.cyan(name)}`);
109
+ }
110
+ console.log(chalk.dim('\n Usage: stealth profile create myprofile --preset us-desktop\n'));
111
+ });
112
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * stealth proxy - Manage proxy pool
3
+ */
4
+
5
+ import ora from 'ora';
6
+ import chalk from 'chalk';
7
+ import {
8
+ addProxy, removeProxy, listProxies,
9
+ testProxy, testAllProxies, poolSize,
10
+ } from '../proxy-pool.js';
11
+ import { log } from '../output.js';
12
+
13
+ export function registerProxy(program) {
14
+ const proxy = program
15
+ .command('proxy')
16
+ .description('Manage proxy pool');
17
+
18
+ // stealth proxy add <url>
19
+ proxy
20
+ .command('add')
21
+ .description('Add a proxy to the pool')
22
+ .argument('<url>', 'Proxy URL (http://user:pass@host:port)')
23
+ .option('--label <label>', 'Label for this proxy')
24
+ .option('--region <region>', 'Geographic region (e.g. US, EU, Asia)')
25
+ .action((url, opts) => {
26
+ try {
27
+ const count = addProxy(url, opts);
28
+ log.success(`Proxy added (${count} total in pool)`);
29
+ } catch (err) {
30
+ log.error(err.message);
31
+ }
32
+ });
33
+
34
+ // stealth proxy remove <url>
35
+ proxy
36
+ .command('remove')
37
+ .description('Remove a proxy from the pool')
38
+ .argument('<url>', 'Proxy URL or label')
39
+ .action((url) => {
40
+ try {
41
+ removeProxy(url);
42
+ log.success('Proxy removed');
43
+ } catch (err) {
44
+ log.error(err.message);
45
+ }
46
+ });
47
+
48
+ // stealth proxy list
49
+ proxy
50
+ .command('list')
51
+ .description('List all proxies')
52
+ .action(() => {
53
+ const proxies = listProxies();
54
+ if (proxies.length === 0) {
55
+ log.info('No proxies configured. Add one with: stealth proxy add <url>');
56
+ return;
57
+ }
58
+
59
+ console.log(chalk.bold('\n Proxy Pool:\n'));
60
+ const header = ` ${'URL'.padEnd(40)} ${'Label'.padEnd(10)} ${'Status'.padEnd(8)} ${'Latency'.padEnd(10)} ${'Uses'.padEnd(6)} ${'Fails'.padEnd(6)}`;
61
+ console.log(chalk.dim(header));
62
+ console.log(chalk.dim(' ' + '─'.repeat(84)));
63
+
64
+ for (const p of proxies) {
65
+ const statusColor = p.status === 'ok' ? chalk.green : p.status === 'fail' ? chalk.red : chalk.dim;
66
+ console.log(
67
+ ` ${p.url.padEnd(40).slice(0, 40)} ${p.label.padEnd(10)} ${statusColor(p.status.padEnd(8))} ${p.latency.padEnd(10)} ${String(p.useCount).padEnd(6)} ${String(p.failCount).padEnd(6)}`,
68
+ );
69
+ }
70
+ console.log();
71
+ });
72
+
73
+ // stealth proxy test [url]
74
+ proxy
75
+ .command('test')
76
+ .description('Test proxy connectivity')
77
+ .argument('[url]', 'Specific proxy URL to test (or test all)')
78
+ .action(async (url) => {
79
+ if (url) {
80
+ const spinner = ora(`Testing ${url}...`).start();
81
+ const result = await testProxy(url);
82
+ spinner.stop();
83
+
84
+ if (result.ok) {
85
+ log.success(`Proxy OK — IP: ${result.ip}, Latency: ${result.latency}ms`);
86
+ } else {
87
+ log.error(`Proxy FAILED — ${result.error}`);
88
+ }
89
+ } else {
90
+ const size = poolSize();
91
+ if (size === 0) {
92
+ log.info('No proxies to test');
93
+ return;
94
+ }
95
+
96
+ const spinner = ora(`Testing ${size} proxies...`).start();
97
+ const results = await testAllProxies();
98
+ spinner.stop();
99
+
100
+ let ok = 0;
101
+ let fail = 0;
102
+ for (const r of results) {
103
+ if (r.ok) {
104
+ ok++;
105
+ log.success(`${r.proxy} — IP: ${r.ip}, ${r.latency}ms`);
106
+ } else {
107
+ fail++;
108
+ log.error(`${r.proxy} — ${r.error}`);
109
+ }
110
+ }
111
+
112
+ console.log();
113
+ log.info(`Results: ${chalk.green(`${ok} ok`)} / ${chalk.red(`${fail} failed`)} / ${size} total`);
114
+ }
115
+ });
116
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * stealth screenshot <url> - Take a screenshot of a page
3
+ */
4
+
5
+ import ora from 'ora';
6
+ import { launchBrowser, closeBrowser, navigate, takeScreenshot, getUrl, waitForReady } from '../browser.js';
7
+ import { log } from '../output.js';
8
+
9
+ export function registerScreenshot(program) {
10
+ program
11
+ .command('screenshot')
12
+ .description('Take a screenshot of a page')
13
+ .argument('<url>', 'URL to screenshot')
14
+ .option('-o, --output <file>', 'Output file path', 'screenshot.png')
15
+ .option('--full', 'Capture full page (not just viewport)')
16
+ .option('--width <px>', 'Viewport width', '1280')
17
+ .option('--height <px>', 'Viewport height', '720')
18
+ .option('--wait <ms>', 'Wait time after page load (ms)', '2000')
19
+ .option('--proxy <proxy>', 'Proxy server')
20
+ .option('--cookies <file>', 'Load cookies from Netscape-format file')
21
+ .option('--no-headless', 'Show browser window')
22
+ .option('--quality <n>', 'JPEG quality (1-100), only for .jpg output')
23
+ .option('--humanize', 'Enable human behavior simulation')
24
+ .option('--retries <n>', 'Max retries on failure', '2')
25
+ .option('--profile <name>', 'Use a browser profile')
26
+ .option('--session <name>', 'Use/restore a named session')
27
+ .option('--proxy-rotate', 'Rotate proxy from pool')
28
+ .action(async (url, opts) => {
29
+ const spinner = ora('Launching stealth browser...').start();
30
+ let handle;
31
+
32
+ try {
33
+ handle = await launchBrowser({
34
+ headless: opts.headless,
35
+ proxy: opts.proxy,
36
+ proxyRotate: opts.proxyRotate,
37
+ profile: opts.profile,
38
+ session: opts.session,
39
+ viewport: {
40
+ width: parseInt(opts.width),
41
+ height: parseInt(opts.height),
42
+ },
43
+ });
44
+
45
+ // Load cookies (direct mode only)
46
+ if (opts.cookies && !handle.isDaemon) {
47
+ const { loadCookies } = await import('../cookies.js');
48
+ await loadCookies(handle.context, opts.cookies);
49
+ }
50
+
51
+ spinner.text = `Navigating to ${url}...`;
52
+ await navigate(handle, url, {
53
+ humanize: opts.humanize,
54
+ retries: parseInt(opts.retries),
55
+ });
56
+
57
+ if (!handle.isDaemon) {
58
+ await waitForReady(handle.page, { timeout: parseInt(opts.wait) });
59
+ }
60
+
61
+ spinner.text = 'Taking screenshot...';
62
+
63
+ if (handle.isDaemon) {
64
+ // Daemon mode: get base64 and write to file
65
+ await takeScreenshot(handle, { path: opts.output, fullPage: opts.full || false });
66
+ } else {
67
+ // Direct mode: native screenshot
68
+ const screenshotOpts = {
69
+ path: opts.output,
70
+ fullPage: opts.full || false,
71
+ };
72
+
73
+ if (opts.output.endsWith('.jpg') || opts.output.endsWith('.jpeg')) {
74
+ screenshotOpts.type = 'jpeg';
75
+ if (opts.quality) screenshotOpts.quality = parseInt(opts.quality);
76
+ } else {
77
+ screenshotOpts.type = 'png';
78
+ }
79
+
80
+ await handle.page.screenshot(screenshotOpts);
81
+ }
82
+
83
+ spinner.stop();
84
+ const currentUrl = await getUrl(handle);
85
+ log.success(`Screenshot saved: ${opts.output}`);
86
+ log.dim(` URL: ${currentUrl}`);
87
+ log.dim(` Size: ${opts.width}x${opts.height}${opts.full ? ' (full page)' : ''}`);
88
+ } catch (err) {
89
+ spinner.stop();
90
+ log.error(`Screenshot failed: ${err.message}`);
91
+ process.exit(1);
92
+ } finally {
93
+ if (handle) await closeBrowser(handle);
94
+ }
95
+ });
96
+ }