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,103 @@
1
+ /**
2
+ * GitHub Search extractor
3
+ */
4
+
5
+ export const name = 'github';
6
+
7
+ export function canHandle(url) {
8
+ return /github\.com\/search/.test(url);
9
+ }
10
+
11
+ export async function waitForResults(page) {
12
+ const selectors = [
13
+ '[data-testid="results-list"]',
14
+ '.repo-list',
15
+ '[data-testid="result"]',
16
+ '.search-title',
17
+ '.Box-row', // Modern GitHub search layout
18
+ 'div[data-testid="result"]',
19
+ ];
20
+
21
+ for (const sel of selectors) {
22
+ try {
23
+ await page.waitForSelector(sel, { timeout: 3000 });
24
+ return;
25
+ } catch {}
26
+ }
27
+
28
+ // GitHub search can be slow, give it more time
29
+ await new Promise((r) => setTimeout(r, 3000));
30
+ }
31
+
32
+ export async function extractResults(page, maxResults = 10) {
33
+ await waitForResults(page);
34
+
35
+ return page.evaluate((max) => {
36
+ const results = [];
37
+
38
+ // Modern GitHub search (React)
39
+ const items = document.querySelectorAll(
40
+ '[data-testid="results-list"] > div, .search-title, .repo-list-item, [data-testid="result"]',
41
+ );
42
+
43
+ for (const item of items) {
44
+ if (results.length >= max) break;
45
+
46
+ // Repository link — try multiple selectors
47
+ const linkEl = item.querySelector(
48
+ 'a[href*="github.com/"]:has(.search-match), a.v-align-middle, a[data-testid="result-title-link"]',
49
+ );
50
+
51
+ if (linkEl) {
52
+ const descEl = item.querySelector('p, .mb-1, [data-testid="result-description"]');
53
+ const langEl = item.querySelector('[itemprop="programmingLanguage"], span.repo-language-color + span');
54
+ const starsEl = item.querySelector('a[href*="/stargazers"], [aria-label*="star"]');
55
+ const updatedEl = item.querySelector('relative-time, [datetime]');
56
+
57
+ results.push({
58
+ title: linkEl.textContent?.trim() || '',
59
+ url: linkEl.href,
60
+ description: descEl?.textContent?.trim() || '',
61
+ language: langEl?.textContent?.trim() || '',
62
+ stars: starsEl?.textContent?.trim() || '',
63
+ updated: updatedEl?.getAttribute('datetime') || updatedEl?.textContent?.trim() || '',
64
+ });
65
+ continue;
66
+ }
67
+
68
+ // Fallback: any link to a repo pattern /user/repo
69
+ const anyLink = item.querySelector('a[href*="github.com/"]');
70
+ if (anyLink && anyLink.href.match(/github\.com\/[\w-]+\/[\w.-]+$/)) {
71
+ const descEl = item.querySelector('p, .mb-1');
72
+ results.push({
73
+ title: anyLink.textContent?.trim() || '',
74
+ url: anyLink.href,
75
+ description: descEl?.textContent?.trim() || '',
76
+ });
77
+ }
78
+ }
79
+
80
+ // Strategy 2: Fallback — grab all repo-like links on the page
81
+ if (results.length === 0) {
82
+ const allLinks = document.querySelectorAll('a[href]');
83
+ const seenUrls = new Set();
84
+ for (const link of allLinks) {
85
+ if (results.length >= max) break;
86
+ const href = link.href;
87
+ if (
88
+ href?.match(/github\.com\/[\w-]+\/[\w.-]+$/) &&
89
+ !href.includes('/search') &&
90
+ !seenUrls.has(href)
91
+ ) {
92
+ seenUrls.add(href);
93
+ const text = link.textContent?.trim();
94
+ if (text && text.length > 2 && text.includes('/')) {
95
+ results.push({ title: text, url: href, description: '' });
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ return results;
102
+ }, maxResults);
103
+ }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Google Search extractor
3
+ *
4
+ * Strategy:
5
+ * 1. Navigate to google.com homepage first (not direct search URL)
6
+ * 2. Type query into search box like a human
7
+ * 3. Wait for async SERP rendering
8
+ * 4. Extract structured results from multiple layout variants
9
+ */
10
+
11
+ import { humanType, humanScroll, randomDelay } from '../humanize.js';
12
+
13
+ export const name = 'google';
14
+
15
+ export function canHandle(url) {
16
+ return /google\.\w+\/search/.test(url);
17
+ }
18
+
19
+ /**
20
+ * Perform Google search with human-like behavior
21
+ * Instead of direct URL, simulates typing into search box
22
+ *
23
+ * @param {Page} page - Playwright page
24
+ * @param {string} query - Search query
25
+ * @returns {boolean} success
26
+ */
27
+ export async function humanSearch(page, query) {
28
+ // Step 1: Visit Google homepage
29
+ await page.goto('https://www.google.com', {
30
+ waitUntil: 'domcontentloaded',
31
+ timeout: 15000,
32
+ });
33
+ await randomDelay(800, 2000);
34
+
35
+ // Step 2: Handle cookie consent (EU/UK)
36
+ try {
37
+ const consentBtn = page.locator(
38
+ 'button:has-text("Accept all"), button:has-text("Accept"), button:has-text("Agree"), button:has-text("I agree"), [id*="accept"], [aria-label*="Accept"]',
39
+ );
40
+ if (await consentBtn.first().isVisible({ timeout: 1500 })) {
41
+ await consentBtn.first().click({ timeout: 2000 });
42
+ await randomDelay(500, 1000);
43
+ }
44
+ } catch {}
45
+
46
+ // Step 3: Type query with human timing
47
+ try {
48
+ await humanType(page, 'textarea[name="q"], input[name="q"]', query, {
49
+ pressEnter: true,
50
+ });
51
+ return true;
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Wait for Google results to render (they load asynchronously)
59
+ */
60
+ export async function waitForResults(page) {
61
+ // Multiple selectors for different Google layouts
62
+ const selectors = [
63
+ '#rso h3',
64
+ '#search h3',
65
+ '#rso [data-snhf]',
66
+ '#rso a[href]:not([href^="/search"])',
67
+ '.g h3',
68
+ ];
69
+
70
+ for (const sel of selectors) {
71
+ try {
72
+ await page.waitForSelector(sel, { timeout: 3000 });
73
+ return;
74
+ } catch {}
75
+ }
76
+
77
+ // Last resort: wait for any meaningful content
78
+ await randomDelay(2000, 3000);
79
+ }
80
+
81
+ /**
82
+ * Check if Google blocked us (CAPTCHA / sorry page)
83
+ */
84
+ export function isBlocked(url) {
85
+ return url.includes('/sorry/') || url.includes('consent.google') || url.includes('/recaptcha/');
86
+ }
87
+
88
+ /**
89
+ * Extract Google search results
90
+ */
91
+ export async function extractResults(page, maxResults = 10) {
92
+ await waitForResults(page);
93
+
94
+ return page.evaluate((max) => {
95
+ const results = [];
96
+
97
+ // Strategy 1: Standard search result cards
98
+ const items = document.querySelectorAll(
99
+ '#rso .g, #rso [data-sokoban-container], #rso [data-snhf], #search .g, .MjjYud',
100
+ );
101
+
102
+ for (const item of items) {
103
+ if (results.length >= max) break;
104
+
105
+ const linkEl = item.querySelector(
106
+ 'a[href]:not([href^="/search"]):not([href^="#"]):not([href*="google.com/search"])',
107
+ );
108
+ const titleEl = item.querySelector('h3');
109
+ const snippetEl = item.querySelector(
110
+ '[data-sncf], .VwiC3b, [style*="-webkit-line-clamp"], .IsZvec, .lEBKkf',
111
+ );
112
+
113
+ if (linkEl && titleEl) {
114
+ const href = linkEl.href;
115
+ if (href && href.startsWith('http') && !href.includes('google.com/search')) {
116
+ results.push({
117
+ title: titleEl.textContent?.trim() || '',
118
+ url: href,
119
+ snippet: snippetEl?.textContent?.trim() || '',
120
+ });
121
+ }
122
+ }
123
+ }
124
+
125
+ // Strategy 2: Fallback — look for any external links with headings
126
+ if (results.length === 0) {
127
+ const fallbackLinks = document.querySelectorAll('#rso a[href], #search a[href]');
128
+ const seenUrls = new Set();
129
+
130
+ for (const link of fallbackLinks) {
131
+ if (results.length >= max) break;
132
+ const href = link.href;
133
+ if (
134
+ href?.startsWith('http') &&
135
+ !href.includes('google.') &&
136
+ !href.includes('gstatic.') &&
137
+ !seenUrls.has(href)
138
+ ) {
139
+ seenUrls.add(href);
140
+ const heading = link.querySelector('h3, h2, [role="heading"]');
141
+ const text = heading?.textContent?.trim() || link.textContent?.trim();
142
+ if (text && text.length > 3) {
143
+ results.push({ title: text.slice(0, 200), url: href, snippet: '' });
144
+ }
145
+ }
146
+ }
147
+ }
148
+
149
+ return results;
150
+ }, maxResults);
151
+ }
152
+
153
+ /**
154
+ * Extract "People also ask" questions
155
+ */
156
+ export async function extractPeopleAlsoAsk(page, maxResults = 5) {
157
+ return page.evaluate((max) => {
158
+ const questions = [];
159
+ const items = document.querySelectorAll(
160
+ '[data-sgrd] [role="button"], .related-question-pair, [jsname="Cpkphb"]',
161
+ );
162
+
163
+ for (const item of items) {
164
+ if (questions.length >= max) break;
165
+ const text = item.textContent?.trim();
166
+ if (text && text.length > 10) {
167
+ questions.push(text);
168
+ }
169
+ }
170
+
171
+ return questions;
172
+ }, maxResults);
173
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Extractor registry — auto-selects the best extractor for a URL/engine
3
+ */
4
+
5
+ import * as google from './google.js';
6
+ import * as duckduckgo from './duckduckgo.js';
7
+ import * as bing from './bing.js';
8
+ import * as github from './github.js';
9
+ import * as youtube from './youtube.js';
10
+ import * as base from './base.js';
11
+
12
+ const extractors = [google, duckduckgo, bing, github, youtube];
13
+
14
+ // Map engine names to extractors
15
+ const engineMap = {
16
+ google: google,
17
+ duckduckgo: duckduckgo,
18
+ bing: bing,
19
+ github: github,
20
+ youtube: youtube,
21
+ };
22
+
23
+ /**
24
+ * Get the best extractor for a given engine name
25
+ *
26
+ * @param {string} engine - Engine name (google, bing, etc.)
27
+ * @returns {object} Extractor module
28
+ */
29
+ export function getExtractorByEngine(engine) {
30
+ return engineMap[engine.toLowerCase()] || base;
31
+ }
32
+
33
+ /**
34
+ * Get the best extractor for a given URL
35
+ *
36
+ * @param {string} url - Page URL
37
+ * @returns {object} Extractor module
38
+ */
39
+ export function getExtractorByUrl(url) {
40
+ for (const extractor of extractors) {
41
+ if (extractor.canHandle(url)) {
42
+ return extractor;
43
+ }
44
+ }
45
+ return base;
46
+ }
47
+
48
+ /**
49
+ * List all available extractors
50
+ */
51
+ export function listExtractors() {
52
+ return [...extractors.map((e) => e.name), base.name];
53
+ }
54
+
55
+ export { google, duckduckgo, bing, github, youtube, base };
@@ -0,0 +1,87 @@
1
+ /**
2
+ * YouTube Search extractor
3
+ */
4
+
5
+ export const name = 'youtube';
6
+
7
+ export function canHandle(url) {
8
+ return /youtube\.com\/results/.test(url);
9
+ }
10
+
11
+ export async function waitForResults(page) {
12
+ try {
13
+ await page.waitForSelector(
14
+ 'ytd-video-renderer, ytd-channel-renderer, #contents ytd-item-section-renderer',
15
+ { timeout: 6000 },
16
+ );
17
+ } catch {}
18
+
19
+ // YouTube loads results dynamically; give extra time
20
+ await new Promise((r) => setTimeout(r, 1500));
21
+ }
22
+
23
+ export async function extractResults(page, maxResults = 10) {
24
+ await waitForResults(page);
25
+
26
+ return page.evaluate((max) => {
27
+ const results = [];
28
+
29
+ // Video renderers
30
+ const items = document.querySelectorAll(
31
+ 'ytd-video-renderer, ytd-channel-renderer, ytd-playlist-renderer',
32
+ );
33
+
34
+ for (const item of items) {
35
+ if (results.length >= max) break;
36
+
37
+ const isChannel = item.tagName === 'YTD-CHANNEL-RENDERER';
38
+ const isPlaylist = item.tagName === 'YTD-PLAYLIST-RENDERER';
39
+
40
+ // Video title link
41
+ const titleLink = item.querySelector(
42
+ 'a#video-title, h3 a, a.channel-link, a[href*="/watch"], a[href*="/playlist"]',
43
+ );
44
+ if (!titleLink) continue;
45
+
46
+ const title = titleLink.textContent?.trim() || titleLink.getAttribute('title') || '';
47
+ const url = titleLink.href;
48
+
49
+ if (!url || !title) continue;
50
+
51
+ // Channel name
52
+ const channelEl = item.querySelector(
53
+ 'ytd-channel-name a, .ytd-channel-name a, #channel-info a, #text.ytd-channel-name',
54
+ );
55
+
56
+ // View count and upload date
57
+ const metaItems = item.querySelectorAll(
58
+ '#metadata-line span, .inline-metadata-item, .ytd-video-meta-block span',
59
+ );
60
+ const views = metaItems[0]?.textContent?.trim() || '';
61
+ const uploadDate = metaItems[1]?.textContent?.trim() || '';
62
+
63
+ // Duration
64
+ const durationEl = item.querySelector(
65
+ 'span.ytd-thumbnail-overlay-time-status-renderer, badge-shape .badge-shape-wiz__text',
66
+ );
67
+
68
+ // Thumbnail
69
+ const thumbEl = item.querySelector('img#img, ytd-thumbnail img');
70
+
71
+ const result = {
72
+ type: isChannel ? 'channel' : isPlaylist ? 'playlist' : 'video',
73
+ title,
74
+ url,
75
+ channel: channelEl?.textContent?.trim() || '',
76
+ views,
77
+ uploadDate,
78
+ duration: durationEl?.textContent?.trim() || '',
79
+ thumbnail: thumbEl?.src || '',
80
+ };
81
+
82
+ results.push(result);
83
+ }
84
+
85
+ return results;
86
+ }, maxResults);
87
+ }
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Human behavior simulation — makes browser automation less detectable
3
+ */
4
+
5
+ /**
6
+ * Random delay between min and max ms
7
+ */
8
+ export function randomDelay(min = 200, max = 800) {
9
+ const delay = min + Math.random() * (max - min);
10
+ return new Promise((resolve) => setTimeout(resolve, delay));
11
+ }
12
+
13
+ /**
14
+ * Simulate human-like scrolling pattern
15
+ * - Variable speed
16
+ * - Occasional pauses
17
+ * - Sometimes scroll back up slightly
18
+ */
19
+ export async function humanScroll(page, opts = {}) {
20
+ const { scrolls = 3, direction = 'down' } = opts;
21
+
22
+ for (let i = 0; i < scrolls; i++) {
23
+ // Variable scroll amount (200-600px)
24
+ const amount = 200 + Math.random() * 400;
25
+ const delta = direction === 'up' ? -amount : amount;
26
+
27
+ await page.mouse.wheel(0, delta);
28
+
29
+ // Random pause between scrolls (300-1200ms)
30
+ await randomDelay(300, 1200);
31
+
32
+ // 20% chance to scroll back slightly (human-like hesitation)
33
+ if (Math.random() < 0.2 && i < scrolls - 1) {
34
+ const backAmount = 50 + Math.random() * 100;
35
+ await page.mouse.wheel(0, -backAmount * Math.sign(delta));
36
+ await randomDelay(200, 500);
37
+ }
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Move mouse along a bezier curve (more natural than straight line)
43
+ */
44
+ export async function humanMouseMove(page, targetX, targetY, opts = {}) {
45
+ const { steps = 15 } = opts;
46
+
47
+ // Get current or random starting position
48
+ const startX = 100 + Math.random() * 400;
49
+ const startY = 100 + Math.random() * 300;
50
+
51
+ // Generate control points for bezier curve
52
+ const cp1x = startX + (targetX - startX) * 0.3 + (Math.random() - 0.5) * 100;
53
+ const cp1y = startY + (targetY - startY) * 0.1 + (Math.random() - 0.5) * 100;
54
+ const cp2x = startX + (targetX - startX) * 0.7 + (Math.random() - 0.5) * 80;
55
+ const cp2y = startY + (targetY - startY) * 0.9 + (Math.random() - 0.5) * 80;
56
+
57
+ for (let i = 0; i <= steps; i++) {
58
+ const t = i / steps;
59
+
60
+ // Cubic bezier interpolation
61
+ const x = cubicBezier(t, startX, cp1x, cp2x, targetX);
62
+ const y = cubicBezier(t, startY, cp1y, cp2y, targetY);
63
+
64
+ await page.mouse.move(x, y);
65
+
66
+ // Variable speed: slower at start and end (ease-in-out)
67
+ const speedFactor = Math.sin(t * Math.PI); // 0→1→0
68
+ const delay = 5 + (1 - speedFactor) * 15;
69
+ await new Promise((r) => setTimeout(r, delay));
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Type text with human-like timing
75
+ * - Variable delay between keystrokes
76
+ * - Occasional longer pauses (thinking)
77
+ * - Optionally make typos and correct them
78
+ */
79
+ export async function humanType(page, selector, text, opts = {}) {
80
+ const { typoRate = 0, pressEnter = false } = opts;
81
+
82
+ // Click the input first
83
+ await page.click(selector, { timeout: 5000 });
84
+ await randomDelay(200, 500);
85
+
86
+ // Clear existing content
87
+ await page.fill(selector, '');
88
+ await randomDelay(100, 300);
89
+
90
+ // Type character by character
91
+ for (let i = 0; i < text.length; i++) {
92
+ const char = text[i];
93
+
94
+ // Simulate typo (if enabled)
95
+ if (typoRate > 0 && Math.random() < typoRate) {
96
+ // Type wrong character
97
+ const wrongChar = String.fromCharCode(char.charCodeAt(0) + (Math.random() > 0.5 ? 1 : -1));
98
+ await page.keyboard.type(wrongChar, { delay: 30 + Math.random() * 50 });
99
+ await randomDelay(100, 300);
100
+
101
+ // Delete it
102
+ await page.keyboard.press('Backspace');
103
+ await randomDelay(50, 150);
104
+ }
105
+
106
+ // Type correct character
107
+ await page.keyboard.type(char, { delay: 30 + Math.random() * 80 });
108
+
109
+ // Occasional longer pause (thinking / reading)
110
+ if (Math.random() < 0.05) {
111
+ await randomDelay(300, 800);
112
+ }
113
+ }
114
+
115
+ if (pressEnter) {
116
+ await randomDelay(300, 600);
117
+ await page.keyboard.press('Enter');
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Simulate human click with mouse movement
123
+ */
124
+ export async function humanClick(page, selector, opts = {}) {
125
+ const { timeout = 5000 } = opts;
126
+
127
+ const element = page.locator(selector);
128
+ const box = await element.boundingBox({ timeout });
129
+
130
+ if (!box) {
131
+ // Fallback to regular click
132
+ await element.click({ timeout });
133
+ return;
134
+ }
135
+
136
+ // Move to element with slight offset (humans don't click exact center)
137
+ const offsetX = (Math.random() - 0.5) * box.width * 0.4;
138
+ const offsetY = (Math.random() - 0.5) * box.height * 0.3;
139
+ const targetX = box.x + box.width / 2 + offsetX;
140
+ const targetY = box.y + box.height / 2 + offsetY;
141
+
142
+ await humanMouseMove(page, targetX, targetY);
143
+ await randomDelay(50, 150);
144
+
145
+ // Mouse down → small pause → mouse up (human click duration)
146
+ await page.mouse.down();
147
+ await randomDelay(30, 80);
148
+ await page.mouse.up();
149
+ }
150
+
151
+ /**
152
+ * Warmup routine - perform natural browsing actions before target URL
153
+ * This helps establish a "normal" browsing fingerprint
154
+ */
155
+ export async function warmup(page, opts = {}) {
156
+ const { sites = null, scrollAfter = true } = opts;
157
+
158
+ const defaultSites = [
159
+ 'https://www.wikipedia.org',
160
+ 'https://www.github.com',
161
+ 'https://news.ycombinator.com',
162
+ ];
163
+
164
+ const warmupSites = sites || defaultSites;
165
+
166
+ // Pick 1 random warmup site
167
+ const site = warmupSites[Math.floor(Math.random() * warmupSites.length)];
168
+
169
+ try {
170
+ await page.goto(site, { waitUntil: 'domcontentloaded', timeout: 10000 });
171
+ await randomDelay(1000, 3000);
172
+
173
+ if (scrollAfter) {
174
+ await humanScroll(page, { scrolls: 2 });
175
+ }
176
+
177
+ // Random mouse movement
178
+ await humanMouseMove(page, 300 + Math.random() * 600, 200 + Math.random() * 400);
179
+ await randomDelay(500, 1500);
180
+ } catch {
181
+ // Warmup failure is not critical
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Post-navigation human behavior — run after page loads
187
+ */
188
+ export async function postNavigationBehavior(page, opts = {}) {
189
+ const { scroll = true, mouseMove = true, minDelay = 500, maxDelay = 1500 } = opts;
190
+
191
+ // Wait a natural amount before interacting
192
+ await randomDelay(minDelay, maxDelay);
193
+
194
+ // Slight mouse movement (human looking at page)
195
+ if (mouseMove) {
196
+ await humanMouseMove(page, 300 + Math.random() * 600, 150 + Math.random() * 300);
197
+ }
198
+
199
+ // Natural scroll to show content loading
200
+ if (scroll) {
201
+ await humanScroll(page, { scrolls: 1 });
202
+ }
203
+ }
204
+
205
+ // --- Math helpers ---
206
+
207
+ function cubicBezier(t, p0, p1, p2, p3) {
208
+ const mt = 1 - t;
209
+ return mt * mt * mt * p0 + 3 * mt * mt * t * p1 + 3 * mt * t * t * p2 + t * t * t * p3;
210
+ }
package/src/index.js ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * stealth-cli — Public API (SDK mode)
3
+ */
4
+
5
+ // Core browser
6
+ export {
7
+ launchBrowser, closeBrowser, navigate, getSnapshot,
8
+ getTextContent, getTitle, getUrl, takeScreenshot, evaluate, waitForReady,
9
+ } from './browser.js';
10
+
11
+ // Search
12
+ export { expandMacro, getSupportedEngines } from './macros.js';
13
+ export { getExtractorByEngine, getExtractorByUrl } from './extractors/index.js';
14
+
15
+ // Cookies
16
+ export { parseCookieFile, loadCookies } from './cookies.js';
17
+
18
+ // Retry + Humanize
19
+ export { withRetry, navigateWithRetry } from './retry.js';
20
+ export {
21
+ randomDelay, humanScroll, humanMouseMove,
22
+ humanType, humanClick, warmup, postNavigationBehavior,
23
+ } from './humanize.js';
24
+
25
+ // Profile + Session + Proxy
26
+ export { createProfile, loadProfile, listProfiles, deleteProfile } from './profiles.js';
27
+ export { getSession, saveSession, captureSession, restoreSession, listSessions } from './session.js';
28
+ export { addProxy, removeProxy, listProxies, getNextProxy, getRandomProxy, testProxy } from './proxy-pool.js';
29
+
30
+ // Daemon
31
+ export { isDaemonRunning } from './daemon.js';
32
+ export { daemonStatus, daemonShutdown } from './client.js';
package/src/macros.js ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Search macros - URL templates for common search engines and sites
3
+ * Inspired by camofox-browser (MIT License)
4
+ */
5
+
6
+ const MACROS = {
7
+ google: (q) => `https://www.google.com/search?q=${encodeURIComponent(q)}`,
8
+ youtube: (q) => `https://www.youtube.com/results?search_query=${encodeURIComponent(q)}`,
9
+ amazon: (q) => `https://www.amazon.com/s?k=${encodeURIComponent(q)}`,
10
+ reddit: (q) => `https://www.reddit.com/search?q=${encodeURIComponent(q)}`,
11
+ wikipedia: (q) => `https://en.wikipedia.org/wiki/Special:Search?search=${encodeURIComponent(q)}`,
12
+ twitter: (q) => `https://twitter.com/search?q=${encodeURIComponent(q)}`,
13
+ yelp: (q) => `https://www.yelp.com/search?find_desc=${encodeURIComponent(q)}`,
14
+ linkedin: (q) => `https://www.linkedin.com/search/results/all/?keywords=${encodeURIComponent(q)}`,
15
+ tiktok: (q) => `https://www.tiktok.com/search?q=${encodeURIComponent(q)}`,
16
+ github: (q) => `https://github.com/search?q=${encodeURIComponent(q)}&type=repositories`,
17
+ stackoverflow: (q) => `https://stackoverflow.com/search?q=${encodeURIComponent(q)}`,
18
+ npmjs: (q) => `https://www.npmjs.com/search?q=${encodeURIComponent(q)}`,
19
+ bing: (q) => `https://www.bing.com/search?q=${encodeURIComponent(q)}`,
20
+ duckduckgo: (q) => `https://duckduckgo.com/?q=${encodeURIComponent(q)}`,
21
+ };
22
+
23
+ /**
24
+ * Expand a search macro to a URL
25
+ */
26
+ export function expandMacro(engine, query) {
27
+ const fn = MACROS[engine.toLowerCase()];
28
+ return fn ? fn(query) : null;
29
+ }
30
+
31
+ /**
32
+ * Get list of supported search engines
33
+ */
34
+ export function getSupportedEngines() {
35
+ return Object.keys(MACROS);
36
+ }