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.
- package/LICENSE +21 -0
- package/README.md +295 -0
- package/bin/stealth.js +50 -0
- package/package.json +65 -0
- package/skills/SKILL.md +244 -0
- package/src/browser.js +341 -0
- package/src/client.js +115 -0
- package/src/commands/batch.js +180 -0
- package/src/commands/browse.js +101 -0
- package/src/commands/config.js +85 -0
- package/src/commands/crawl.js +169 -0
- package/src/commands/daemon.js +143 -0
- package/src/commands/extract.js +153 -0
- package/src/commands/fingerprint.js +306 -0
- package/src/commands/interactive.js +284 -0
- package/src/commands/mcp.js +68 -0
- package/src/commands/monitor.js +160 -0
- package/src/commands/pdf.js +109 -0
- package/src/commands/profile.js +112 -0
- package/src/commands/proxy.js +116 -0
- package/src/commands/screenshot.js +96 -0
- package/src/commands/search.js +162 -0
- package/src/commands/serve.js +240 -0
- package/src/config.js +123 -0
- package/src/cookies.js +67 -0
- package/src/daemon-entry.js +19 -0
- package/src/daemon.js +294 -0
- package/src/errors.js +136 -0
- package/src/extractors/base.js +59 -0
- package/src/extractors/bing.js +47 -0
- package/src/extractors/duckduckgo.js +91 -0
- package/src/extractors/github.js +103 -0
- package/src/extractors/google.js +173 -0
- package/src/extractors/index.js +55 -0
- package/src/extractors/youtube.js +87 -0
- package/src/humanize.js +210 -0
- package/src/index.js +32 -0
- package/src/macros.js +36 -0
- package/src/mcp-server.js +341 -0
- package/src/output.js +65 -0
- package/src/profiles.js +308 -0
- package/src/proxy-pool.js +256 -0
- package/src/retry.js +112 -0
- package/src/session.js +159 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stealth fingerprint - Check browser fingerprint and anti-detection status
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { launchBrowser, closeBrowser, navigate, evaluate, waitForReady } from '../browser.js';
|
|
8
|
+
import { log } from '../output.js';
|
|
9
|
+
|
|
10
|
+
export function registerFingerprint(program) {
|
|
11
|
+
program
|
|
12
|
+
.command('fingerprint')
|
|
13
|
+
.description('Check browser fingerprint and anti-detection status')
|
|
14
|
+
.option('--profile <name>', 'Use a browser profile')
|
|
15
|
+
.option('--proxy <proxy>', 'Proxy server')
|
|
16
|
+
.option('--json', 'Output as JSON')
|
|
17
|
+
.option('--check', 'Run anti-detection tests against bot detection sites')
|
|
18
|
+
.option('--compare <n>', 'Launch N times and compare fingerprints for uniqueness', '1')
|
|
19
|
+
.option('--no-headless', 'Show browser window')
|
|
20
|
+
.action(async (opts) => {
|
|
21
|
+
const compareCount = parseInt(opts.compare);
|
|
22
|
+
|
|
23
|
+
if (compareCount > 1) {
|
|
24
|
+
await compareFingerprints(compareCount, opts);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (opts.check) {
|
|
29
|
+
await runDetectionTests(opts);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Default: show current fingerprint
|
|
34
|
+
await showFingerprint(opts);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function showFingerprint(opts) {
|
|
39
|
+
const spinner = ora('Collecting fingerprint...').start();
|
|
40
|
+
let handle;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
handle = await launchBrowser({
|
|
44
|
+
headless: opts.headless,
|
|
45
|
+
profile: opts.profile,
|
|
46
|
+
proxy: opts.proxy,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (handle.isDaemon) {
|
|
50
|
+
spinner.stop();
|
|
51
|
+
log.error('Fingerprint check requires direct mode');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
await navigate(handle, 'about:blank');
|
|
56
|
+
|
|
57
|
+
const fp = await evaluate(handle, `(() => {
|
|
58
|
+
return {
|
|
59
|
+
userAgent: navigator.userAgent,
|
|
60
|
+
platform: navigator.platform,
|
|
61
|
+
language: navigator.language,
|
|
62
|
+
languages: navigator.languages,
|
|
63
|
+
hardwareConcurrency: navigator.hardwareConcurrency,
|
|
64
|
+
deviceMemory: navigator.deviceMemory || 'N/A',
|
|
65
|
+
maxTouchPoints: navigator.maxTouchPoints,
|
|
66
|
+
cookieEnabled: navigator.cookieEnabled,
|
|
67
|
+
doNotTrack: navigator.doNotTrack,
|
|
68
|
+
screenWidth: screen.width,
|
|
69
|
+
screenHeight: screen.height,
|
|
70
|
+
screenColorDepth: screen.colorDepth,
|
|
71
|
+
screenPixelDepth: screen.pixelDepth,
|
|
72
|
+
outerWidth: window.outerWidth,
|
|
73
|
+
outerHeight: window.outerHeight,
|
|
74
|
+
innerWidth: window.innerWidth,
|
|
75
|
+
innerHeight: window.innerHeight,
|
|
76
|
+
devicePixelRatio: window.devicePixelRatio,
|
|
77
|
+
timezoneOffset: new Date().getTimezoneOffset(),
|
|
78
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
79
|
+
webdriver: navigator.webdriver,
|
|
80
|
+
pdfViewerEnabled: navigator.pdfViewerEnabled,
|
|
81
|
+
};
|
|
82
|
+
})()`);
|
|
83
|
+
|
|
84
|
+
// WebGL info
|
|
85
|
+
const webgl = await evaluate(handle, `(() => {
|
|
86
|
+
try {
|
|
87
|
+
const canvas = document.createElement('canvas');
|
|
88
|
+
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
|
|
89
|
+
if (!gl) return { renderer: 'N/A', vendor: 'N/A' };
|
|
90
|
+
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
|
|
91
|
+
return {
|
|
92
|
+
renderer: debugInfo ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) : 'hidden',
|
|
93
|
+
vendor: debugInfo ? gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) : 'hidden',
|
|
94
|
+
};
|
|
95
|
+
} catch { return { renderer: 'error', vendor: 'error' }; }
|
|
96
|
+
})()`);
|
|
97
|
+
|
|
98
|
+
spinner.stop();
|
|
99
|
+
|
|
100
|
+
if (opts.json) {
|
|
101
|
+
console.log(JSON.stringify({ ...fp, webgl }, null, 2));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.log(chalk.bold('\n 🦊 Browser Fingerprint\n'));
|
|
106
|
+
|
|
107
|
+
const print = (label, value, warn) => {
|
|
108
|
+
const icon = warn ? chalk.yellow('⚠') : chalk.green('✓');
|
|
109
|
+
console.log(` ${icon} ${chalk.dim(label.padEnd(22))} ${value}`);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
print('User-Agent', fp.userAgent);
|
|
113
|
+
print('Platform', fp.platform);
|
|
114
|
+
print('Language', `${fp.language} [${fp.languages?.join(', ')}]`);
|
|
115
|
+
print('Timezone', `${fp.timezone} (offset: ${fp.timezoneOffset})`);
|
|
116
|
+
print('Screen', `${fp.screenWidth}x${fp.screenHeight} @${fp.devicePixelRatio}x`);
|
|
117
|
+
print('Viewport', `${fp.innerWidth}x${fp.innerHeight}`);
|
|
118
|
+
print('Color Depth', `${fp.screenColorDepth}bit`);
|
|
119
|
+
print('CPU Cores', fp.hardwareConcurrency);
|
|
120
|
+
print('Memory', `${fp.deviceMemory}GB`);
|
|
121
|
+
print('Touch Points', fp.maxTouchPoints);
|
|
122
|
+
print('WebGL Renderer', webgl.renderer);
|
|
123
|
+
print('WebGL Vendor', webgl.vendor);
|
|
124
|
+
print('WebDriver', fp.webdriver ? chalk.red('true (DETECTED!)') : 'false', fp.webdriver);
|
|
125
|
+
print('Do Not Track', fp.doNotTrack || 'unset');
|
|
126
|
+
print('Cookies', fp.cookieEnabled ? 'enabled' : 'disabled');
|
|
127
|
+
|
|
128
|
+
console.log();
|
|
129
|
+
|
|
130
|
+
if (fp.webdriver) {
|
|
131
|
+
log.warn('navigator.webdriver is true — this is a detection flag!');
|
|
132
|
+
} else {
|
|
133
|
+
log.success('No obvious detection flags found');
|
|
134
|
+
}
|
|
135
|
+
console.log();
|
|
136
|
+
} catch (err) {
|
|
137
|
+
spinner.stop();
|
|
138
|
+
log.error(`Fingerprint check failed: ${err.message}`);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
} finally {
|
|
141
|
+
if (handle) await closeBrowser(handle);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function runDetectionTests(opts) {
|
|
146
|
+
const spinner = ora('Running anti-detection tests...').start();
|
|
147
|
+
let handle;
|
|
148
|
+
|
|
149
|
+
const tests = [
|
|
150
|
+
{
|
|
151
|
+
name: 'Bot Detection (CreepJS)',
|
|
152
|
+
url: 'https://abrahamjuliot.github.io/creepjs/',
|
|
153
|
+
check: async (page) => {
|
|
154
|
+
await page.waitForTimeout(5000);
|
|
155
|
+
const score = await page.$eval('#fingerprint-data .visitor-info', (el) => el.textContent).catch(() => null);
|
|
156
|
+
return { score: score || 'Unable to extract — check manually' };
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'WebDriver Detection',
|
|
161
|
+
url: 'about:blank',
|
|
162
|
+
check: async (page) => {
|
|
163
|
+
const webdriver = await page.evaluate(() => navigator.webdriver);
|
|
164
|
+
return {
|
|
165
|
+
detected: webdriver,
|
|
166
|
+
status: webdriver ? 'FAIL — navigator.webdriver is true' : 'PASS',
|
|
167
|
+
};
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: 'Browser Fingerprint Consistency',
|
|
172
|
+
url: 'about:blank',
|
|
173
|
+
check: async (page) => {
|
|
174
|
+
const data = await page.evaluate(() => {
|
|
175
|
+
const ua = navigator.userAgent;
|
|
176
|
+
const platform = navigator.platform;
|
|
177
|
+
const isWindows = ua.includes('Windows');
|
|
178
|
+
const isMac = ua.includes('Macintosh');
|
|
179
|
+
const isLinux = ua.includes('Linux');
|
|
180
|
+
|
|
181
|
+
const platformMatch =
|
|
182
|
+
(isWindows && platform.startsWith('Win')) ||
|
|
183
|
+
(isMac && platform.startsWith('Mac')) ||
|
|
184
|
+
(isLinux && platform.startsWith('Linux'));
|
|
185
|
+
|
|
186
|
+
return { ua: ua.slice(0, 60), platform, consistent: platformMatch };
|
|
187
|
+
});
|
|
188
|
+
return {
|
|
189
|
+
...data,
|
|
190
|
+
status: data.consistent ? 'PASS — UA matches platform' : 'WARN — UA/platform mismatch',
|
|
191
|
+
};
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
handle = await launchBrowser({
|
|
198
|
+
headless: opts.headless,
|
|
199
|
+
profile: opts.profile,
|
|
200
|
+
proxy: opts.proxy,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (handle.isDaemon) {
|
|
204
|
+
spinner.stop();
|
|
205
|
+
log.error('Detection tests require direct mode');
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
spinner.stop();
|
|
210
|
+
console.log(chalk.bold('\n 🔍 Anti-Detection Test Results\n'));
|
|
211
|
+
|
|
212
|
+
const results = [];
|
|
213
|
+
|
|
214
|
+
for (const test of tests) {
|
|
215
|
+
process.stderr.write(chalk.dim(` Testing: ${test.name}...`));
|
|
216
|
+
try {
|
|
217
|
+
await handle.page.goto(test.url, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
218
|
+
await waitForReady(handle.page, { timeout: 3000 });
|
|
219
|
+
const result = await test.check(handle.page);
|
|
220
|
+
results.push({ name: test.name, ...result });
|
|
221
|
+
|
|
222
|
+
const icon = result.status?.startsWith('PASS') ? chalk.green(' ✓') :
|
|
223
|
+
result.status?.startsWith('FAIL') ? chalk.red(' ✗') : chalk.yellow(' ⚠');
|
|
224
|
+
console.log(`\r ${icon} ${chalk.bold(test.name)}`);
|
|
225
|
+
|
|
226
|
+
for (const [key, val] of Object.entries(result)) {
|
|
227
|
+
console.log(chalk.dim(` ${key}: ${val}`));
|
|
228
|
+
}
|
|
229
|
+
} catch (err) {
|
|
230
|
+
console.log(`\r ${chalk.red('✗')} ${test.name}: ${err.message}`);
|
|
231
|
+
results.push({ name: test.name, error: err.message });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
console.log();
|
|
236
|
+
|
|
237
|
+
if (opts.json) {
|
|
238
|
+
console.log(JSON.stringify(results, null, 2));
|
|
239
|
+
}
|
|
240
|
+
} catch (err) {
|
|
241
|
+
spinner.stop();
|
|
242
|
+
log.error(`Detection tests failed: ${err.message}`);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
} finally {
|
|
245
|
+
if (handle) await closeBrowser(handle);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function compareFingerprints(count, opts) {
|
|
250
|
+
const spinner = ora(`Comparing ${count} fingerprint launches...`).start();
|
|
251
|
+
const fingerprints = [];
|
|
252
|
+
|
|
253
|
+
for (let i = 0; i < count; i++) {
|
|
254
|
+
spinner.text = `Launch ${i + 1}/${count}...`;
|
|
255
|
+
let handle;
|
|
256
|
+
try {
|
|
257
|
+
handle = await launchBrowser({
|
|
258
|
+
headless: opts.headless,
|
|
259
|
+
profile: opts.profile,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
if (handle.isDaemon) {
|
|
263
|
+
spinner.stop();
|
|
264
|
+
log.error('Compare requires direct mode');
|
|
265
|
+
process.exit(1);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const fp = await evaluate(handle, `(() => ({
|
|
269
|
+
ua: navigator.userAgent,
|
|
270
|
+
platform: navigator.platform,
|
|
271
|
+
cores: navigator.hardwareConcurrency,
|
|
272
|
+
memory: navigator.deviceMemory,
|
|
273
|
+
screen: screen.width + 'x' + screen.height,
|
|
274
|
+
tz: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
275
|
+
lang: navigator.language,
|
|
276
|
+
webdriver: navigator.webdriver,
|
|
277
|
+
}))()`);
|
|
278
|
+
|
|
279
|
+
fingerprints.push(fp);
|
|
280
|
+
} finally {
|
|
281
|
+
if (handle) await closeBrowser(handle);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
spinner.stop();
|
|
286
|
+
|
|
287
|
+
console.log(chalk.bold(`\n Fingerprint Comparison (${count} launches)\n`));
|
|
288
|
+
|
|
289
|
+
// Check which fields differ
|
|
290
|
+
const fields = Object.keys(fingerprints[0]);
|
|
291
|
+
for (const field of fields) {
|
|
292
|
+
const values = fingerprints.map((fp) => String(fp[field]));
|
|
293
|
+
const unique = [...new Set(values)];
|
|
294
|
+
const icon = unique.length > 1 ? chalk.green('✓ varies') : chalk.yellow('= same');
|
|
295
|
+
console.log(` ${icon} ${chalk.dim(field.padEnd(12))} ${unique.join(' | ')}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
console.log();
|
|
299
|
+
const varyingFields = fields.filter((f) => new Set(fingerprints.map((fp) => String(fp[f]))).size > 1);
|
|
300
|
+
if (varyingFields.length > 0) {
|
|
301
|
+
log.success(`${varyingFields.length}/${fields.length} fields vary between launches — good fingerprint rotation`);
|
|
302
|
+
} else {
|
|
303
|
+
log.warn('All fields identical — fingerprints are not rotating');
|
|
304
|
+
}
|
|
305
|
+
console.log();
|
|
306
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stealth interactive - Interactive REPL mode for browsing
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createInterface } from 'readline';
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import {
|
|
9
|
+
launchBrowser, closeBrowser, navigate, getSnapshot,
|
|
10
|
+
getTextContent, getUrl, getTitle, evaluate, takeScreenshot, waitForReady,
|
|
11
|
+
} from '../browser.js';
|
|
12
|
+
import { expandMacro, getSupportedEngines } from '../macros.js';
|
|
13
|
+
import { humanClick, humanType, humanScroll, randomDelay } from '../humanize.js';
|
|
14
|
+
import { log } from '../output.js';
|
|
15
|
+
|
|
16
|
+
const HELP_TEXT = `
|
|
17
|
+
${chalk.bold('Navigation:')}
|
|
18
|
+
${chalk.cyan('goto <url>')} Navigate to a URL
|
|
19
|
+
${chalk.cyan('search <engine> <q>')} Search (${getSupportedEngines().slice(0, 5).join(', ')}...)
|
|
20
|
+
${chalk.cyan('back')} Go back
|
|
21
|
+
${chalk.cyan('forward')} Go forward
|
|
22
|
+
${chalk.cyan('reload')} Reload page
|
|
23
|
+
|
|
24
|
+
${chalk.bold('Inspection:')}
|
|
25
|
+
${chalk.cyan('snapshot')} Accessibility snapshot
|
|
26
|
+
${chalk.cyan('text')} Page text content
|
|
27
|
+
${chalk.cyan('title')} Page title
|
|
28
|
+
${chalk.cyan('url')} Current URL
|
|
29
|
+
${chalk.cyan('links')} List all links
|
|
30
|
+
${chalk.cyan('screenshot [file]')} Take a screenshot
|
|
31
|
+
|
|
32
|
+
${chalk.bold('Interaction:')}
|
|
33
|
+
${chalk.cyan('click <selector>')} Click an element
|
|
34
|
+
${chalk.cyan('hclick <selector>')} Human-like click (mouse movement)
|
|
35
|
+
${chalk.cyan('type <sel> <text>')} Type text into element
|
|
36
|
+
${chalk.cyan('htype <sel> <text>')} Human-like typing (variable speed)
|
|
37
|
+
${chalk.cyan('scroll [up|down] [n]')} Scroll page
|
|
38
|
+
${chalk.cyan('eval <js>')} Evaluate JavaScript
|
|
39
|
+
${chalk.cyan('wait <ms>')} Wait for milliseconds
|
|
40
|
+
|
|
41
|
+
${chalk.bold('Other:')}
|
|
42
|
+
${chalk.cyan('help')} Show this help
|
|
43
|
+
${chalk.cyan('exit')} Exit
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
export function registerInteractive(program) {
|
|
47
|
+
program
|
|
48
|
+
.command('interactive')
|
|
49
|
+
.alias('repl')
|
|
50
|
+
.description('Interactive browser REPL mode')
|
|
51
|
+
.option('--proxy <proxy>', 'Proxy server')
|
|
52
|
+
.option('--cookies <file>', 'Load cookies from Netscape-format file')
|
|
53
|
+
.option('--no-headless', 'Show browser window')
|
|
54
|
+
.option('--url <url>', 'Initial URL to open')
|
|
55
|
+
.action(async (opts) => {
|
|
56
|
+
const spinner = ora('Launching stealth browser...').start();
|
|
57
|
+
let handle;
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
handle = await launchBrowser({
|
|
61
|
+
headless: opts.headless,
|
|
62
|
+
proxy: opts.proxy,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (opts.cookies && !handle.isDaemon) {
|
|
66
|
+
const { loadCookies } = await import('../cookies.js');
|
|
67
|
+
const result = await loadCookies(handle.context, opts.cookies);
|
|
68
|
+
log.info(result.message);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (opts.url) {
|
|
72
|
+
await navigate(handle, opts.url);
|
|
73
|
+
if (!handle.isDaemon) {
|
|
74
|
+
await waitForReady(handle.page);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
spinner.stop();
|
|
79
|
+
|
|
80
|
+
console.log(chalk.bold('\n🦊 Stealth Browser Interactive Mode'));
|
|
81
|
+
console.log(chalk.dim('Type "help" for commands, "exit" to quit.\n'));
|
|
82
|
+
|
|
83
|
+
if (opts.url) {
|
|
84
|
+
const currentUrl = await getUrl(handle);
|
|
85
|
+
log.info(`Current page: ${currentUrl}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const rl = createInterface({
|
|
89
|
+
input: process.stdin,
|
|
90
|
+
output: process.stderr,
|
|
91
|
+
prompt: chalk.green('stealth> '),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
rl.prompt();
|
|
95
|
+
|
|
96
|
+
rl.on('line', async (line) => {
|
|
97
|
+
const input = line.trim();
|
|
98
|
+
if (!input) { rl.prompt(); return; }
|
|
99
|
+
|
|
100
|
+
const [cmd, ...args] = input.split(/\s+/);
|
|
101
|
+
const argStr = args.join(' ');
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
switch (cmd.toLowerCase()) {
|
|
105
|
+
case 'goto': case 'go': case 'navigate': {
|
|
106
|
+
if (!argStr) { log.warn('Usage: goto <url>'); break; }
|
|
107
|
+
const s = ora(`Navigating...`).start();
|
|
108
|
+
await navigate(handle, argStr);
|
|
109
|
+
if (!handle.isDaemon) await waitForReady(handle.page);
|
|
110
|
+
s.stop();
|
|
111
|
+
log.success(`→ ${await getUrl(handle)}`);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
case 'search': {
|
|
116
|
+
if (args.length < 2) { log.warn('Usage: search <engine> <query>'); break; }
|
|
117
|
+
const engine = args[0];
|
|
118
|
+
const query = args.slice(1).join(' ');
|
|
119
|
+
const url = expandMacro(engine, query);
|
|
120
|
+
if (!url) { log.error(`Unknown engine. Try: ${getSupportedEngines().join(', ')}`); break; }
|
|
121
|
+
const s = ora(`Searching ${engine}...`).start();
|
|
122
|
+
await navigate(handle, url);
|
|
123
|
+
if (!handle.isDaemon) await waitForReady(handle.page);
|
|
124
|
+
s.stop();
|
|
125
|
+
log.success(`→ ${await getUrl(handle)}`);
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
case 'click': {
|
|
130
|
+
if (!argStr) { log.warn('Usage: click <selector>'); break; }
|
|
131
|
+
if (handle.isDaemon) { log.warn('click requires direct mode'); break; }
|
|
132
|
+
await handle.page.click(argStr, { timeout: 5000 });
|
|
133
|
+
await waitForReady(handle.page, { timeout: 2000 });
|
|
134
|
+
log.success('Clicked');
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
case 'hclick': {
|
|
139
|
+
if (!argStr) { log.warn('Usage: hclick <selector>'); break; }
|
|
140
|
+
if (handle.isDaemon) { log.warn('hclick requires direct mode'); break; }
|
|
141
|
+
await humanClick(handle.page, argStr);
|
|
142
|
+
await waitForReady(handle.page, { timeout: 2000 });
|
|
143
|
+
log.success('Human-clicked');
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
case 'type': {
|
|
148
|
+
if (args.length < 2) { log.warn('Usage: type <selector> <text>'); break; }
|
|
149
|
+
if (handle.isDaemon) { log.warn('type requires direct mode'); break; }
|
|
150
|
+
await handle.page.fill(args[0], args.slice(1).join(' '));
|
|
151
|
+
log.success(`Typed: "${args.slice(1).join(' ')}"`);
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
case 'htype': {
|
|
156
|
+
if (args.length < 2) { log.warn('Usage: htype <selector> <text>'); break; }
|
|
157
|
+
if (handle.isDaemon) { log.warn('htype requires direct mode'); break; }
|
|
158
|
+
await humanType(handle.page, args[0], args.slice(1).join(' '));
|
|
159
|
+
log.success(`Human-typed: "${args.slice(1).join(' ')}"`);
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
case 'scroll': {
|
|
164
|
+
if (handle.isDaemon) { log.warn('scroll requires direct mode'); break; }
|
|
165
|
+
const direction = args[0] || 'down';
|
|
166
|
+
const scrolls = parseInt(args[1]) || 2;
|
|
167
|
+
await humanScroll(handle.page, { direction, scrolls });
|
|
168
|
+
log.success(`Scrolled ${direction}`);
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
case 'snapshot': {
|
|
173
|
+
const snapshot = await getSnapshot(handle);
|
|
174
|
+
console.log(snapshot);
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
case 'text': {
|
|
179
|
+
const text = await getTextContent(handle);
|
|
180
|
+
console.log(text);
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
case 'title': {
|
|
185
|
+
console.log(await getTitle(handle));
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
case 'url': {
|
|
190
|
+
console.log(await getUrl(handle));
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
case 'links': {
|
|
195
|
+
const links = await evaluate(handle, `
|
|
196
|
+
Array.from(document.querySelectorAll('a[href]'))
|
|
197
|
+
.filter(a => a.href.startsWith('http'))
|
|
198
|
+
.slice(0, 30)
|
|
199
|
+
.map(a => ({ text: a.textContent?.trim().slice(0, 60), url: a.href }))
|
|
200
|
+
`);
|
|
201
|
+
if (links) {
|
|
202
|
+
links.forEach((l, i) =>
|
|
203
|
+
console.log(`${chalk.dim(`${i + 1}.`)} ${l.text || '(no text)'}\n ${chalk.cyan(l.url)}`)
|
|
204
|
+
);
|
|
205
|
+
log.dim(`Total: ${links.length} links shown`);
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
case 'screenshot': {
|
|
211
|
+
const file = argStr || 'screenshot.png';
|
|
212
|
+
await takeScreenshot(handle, { path: file });
|
|
213
|
+
log.success(`Screenshot saved: ${file}`);
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
case 'back': {
|
|
218
|
+
if (handle.isDaemon) { log.warn('back requires direct mode'); break; }
|
|
219
|
+
await handle.page.goBack({ timeout: 10000 });
|
|
220
|
+
log.success(`← ${handle.page.url()}`);
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
case 'forward': {
|
|
225
|
+
if (handle.isDaemon) { log.warn('forward requires direct mode'); break; }
|
|
226
|
+
await handle.page.goForward({ timeout: 10000 });
|
|
227
|
+
log.success(`→ ${handle.page.url()}`);
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
case 'reload': {
|
|
232
|
+
if (handle.isDaemon) { log.warn('reload requires direct mode'); break; }
|
|
233
|
+
await handle.page.reload({ timeout: 30000 });
|
|
234
|
+
log.success('Reloaded');
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
case 'eval': case 'js': {
|
|
239
|
+
if (!argStr) { log.warn('Usage: eval <javascript>'); break; }
|
|
240
|
+
const result = await evaluate(handle, argStr);
|
|
241
|
+
console.log(result);
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
case 'wait': {
|
|
246
|
+
const ms = parseInt(argStr) || 1000;
|
|
247
|
+
await randomDelay(ms, ms);
|
|
248
|
+
log.success(`Waited ${ms}ms`);
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
case 'help': case '?': {
|
|
253
|
+
console.log(HELP_TEXT);
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
case 'exit': case 'quit': case 'q': {
|
|
258
|
+
rl.close();
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
default:
|
|
263
|
+
log.warn(`Unknown command: ${cmd}. Type "help" for commands.`);
|
|
264
|
+
}
|
|
265
|
+
} catch (err) {
|
|
266
|
+
log.error(`${err.message}`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
rl.prompt();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
rl.on('close', async () => {
|
|
273
|
+
await closeBrowser(handle);
|
|
274
|
+
log.success('Bye! 🦊');
|
|
275
|
+
process.exit(0);
|
|
276
|
+
});
|
|
277
|
+
} catch (err) {
|
|
278
|
+
spinner.stop();
|
|
279
|
+
log.error(`Failed to start: ${err.message}`);
|
|
280
|
+
if (handle) await closeBrowser(handle);
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stealth mcp - Start MCP server for AI agent integration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { log } from '../output.js';
|
|
7
|
+
|
|
8
|
+
export function registerMcp(program) {
|
|
9
|
+
program
|
|
10
|
+
.command('mcp')
|
|
11
|
+
.description('Start MCP server for Claude Desktop, Cursor, etc.')
|
|
12
|
+
.option('--list-tools', 'List available MCP tools without starting server')
|
|
13
|
+
.option('--config', 'Print MCP configuration JSON for claude_desktop_config.json')
|
|
14
|
+
.action(async (opts) => {
|
|
15
|
+
if (opts.config) {
|
|
16
|
+
printConfig();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (opts.listTools) {
|
|
21
|
+
listTools();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Start MCP server on stdio
|
|
26
|
+
const { McpServer } = await import('../mcp-server.js');
|
|
27
|
+
const server = new McpServer();
|
|
28
|
+
await server.run();
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function printConfig() {
|
|
33
|
+
const config = {
|
|
34
|
+
mcpServers: {
|
|
35
|
+
stealth: {
|
|
36
|
+
command: 'node',
|
|
37
|
+
args: [process.argv[1].replace(/\/commands\/.*/, '/../bin/stealth.js'), 'mcp'],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
console.log(chalk.bold('\n Add this to your claude_desktop_config.json:\n'));
|
|
43
|
+
console.log(JSON.stringify(config, null, 2));
|
|
44
|
+
console.log();
|
|
45
|
+
|
|
46
|
+
log.dim(' Config location:');
|
|
47
|
+
log.dim(' macOS: ~/Library/Application Support/Claude/claude_desktop_config.json');
|
|
48
|
+
log.dim(' Linux: ~/.config/Claude/claude_desktop_config.json');
|
|
49
|
+
console.log();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function listTools() {
|
|
53
|
+
const tools = [
|
|
54
|
+
{ name: 'stealth_browse', desc: 'Visit URL with anti-detection, return text/snapshot' },
|
|
55
|
+
{ name: 'stealth_screenshot', desc: 'Screenshot a page (returns base64 PNG)' },
|
|
56
|
+
{ name: 'stealth_search', desc: 'Search Google/Bing/DuckDuckGo/YouTube/GitHub' },
|
|
57
|
+
{ name: 'stealth_extract', desc: 'Extract links/images/meta/headers/selector' },
|
|
58
|
+
{ name: 'stealth_click', desc: 'Click element by CSS selector' },
|
|
59
|
+
{ name: 'stealth_type', desc: 'Type text into input element' },
|
|
60
|
+
{ name: 'stealth_evaluate', desc: 'Execute JavaScript in page' },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
console.log(chalk.bold('\n MCP Tools:\n'));
|
|
64
|
+
for (const t of tools) {
|
|
65
|
+
console.log(` ${chalk.cyan(t.name.padEnd(24))} ${t.desc}`);
|
|
66
|
+
}
|
|
67
|
+
console.log();
|
|
68
|
+
}
|