human-browser 4.0.0 ā 4.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 +5 -5
- package/SKILL.md +72 -1
- package/package.json +3 -3
- package/scripts/browser-agent.js +616 -0
- package/scripts/browser-human.js +7 -7
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> **No Mac Mini. No local machine. Your agent runs it anywhere.**
|
|
4
4
|
> Residential IPs from 10+ countries. Bypasses Cloudflare, DataDome, PerimeterX.
|
|
5
5
|
>
|
|
6
|
-
> š **Product page:** https://humanbrowser.
|
|
6
|
+
> š **Product page:** https://humanbrowser.cloud
|
|
7
7
|
> š¬ **Support:** https://t.me/virixlabs
|
|
8
8
|
|
|
9
9
|
---
|
|
@@ -31,7 +31,7 @@ Human Browser solves this by combining:
|
|
|
31
31
|
```js
|
|
32
32
|
const { launchHuman } = require('./scripts/browser-human');
|
|
33
33
|
|
|
34
|
-
// š Zero config ā auto-fetches trial credentials from humanbrowser.
|
|
34
|
+
// š Zero config ā auto-fetches trial credentials from humanbrowser.cloud
|
|
35
35
|
const { browser, page, humanType, humanClick, humanScroll, sleep } = await launchHuman();
|
|
36
36
|
// Output: š Human Browser trial activated! (~100MB Romania residential IP)
|
|
37
37
|
|
|
@@ -49,7 +49,7 @@ await humanClick(page, 760, 400);
|
|
|
49
49
|
await browser.close();
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
> **Trial exhausted?** Get a paid plan at https://humanbrowser.
|
|
52
|
+
> **Trial exhausted?** Get a paid plan at https://humanbrowser.cloud, then set `PROXY_USER` / `PROXY_PASS` in your `.env`.
|
|
53
53
|
|
|
54
54
|
---
|
|
55
55
|
|
|
@@ -85,7 +85,7 @@ clawhub install al1enjesus/human-browser
|
|
|
85
85
|
## Proxy Providers
|
|
86
86
|
|
|
87
87
|
### Option 1: Human Browser Managed (recommended)
|
|
88
|
-
Buy directly at **humanbrowser.
|
|
88
|
+
Buy directly at **humanbrowser.cloud** ā we handle everything, from $13.99/mo.
|
|
89
89
|
Supports crypto (USDT/ETH/BTC/SOL) and card. AI agents can auto-purchase.
|
|
90
90
|
|
|
91
91
|
### Option 2: Bring Your Own Proxy
|
|
@@ -125,5 +125,5 @@ PROXY_PASS=your-password
|
|
|
125
125
|
|
|
126
126
|
---
|
|
127
127
|
|
|
128
|
-
ā **Product page + pricing:** https://humanbrowser.
|
|
128
|
+
ā **Product page + pricing:** https://humanbrowser.cloud
|
|
129
129
|
ā **Support & questions:** https://t.me/virixlabs
|
package/SKILL.md
CHANGED
|
@@ -610,7 +610,7 @@ curl -sx "http://USER:PASS@ro.decodo.com:13001" -X POST https://httpbin.org/post
|
|
|
610
610
|
|
|
611
611
|
## Plans & credentials
|
|
612
612
|
|
|
613
|
-
š **https://humanbrowser.
|
|
613
|
+
š **https://humanbrowser.cloud** ā get credentials, manage subscription
|
|
614
614
|
|
|
615
615
|
| Plan | Price | Countries | Bandwidth |
|
|
616
616
|
|------|-------|-----------|-----------|
|
|
@@ -619,3 +619,74 @@ curl -sx "http://USER:PASS@ro.decodo.com:13001" -X POST https://httpbin.org/post
|
|
|
619
619
|
| Enterprise | $299/mo | š Dedicated | Unlimited |
|
|
620
620
|
|
|
621
621
|
Payment: Stripe (card, Apple Pay) or Crypto (USDT TRC-20, BTC, ETH, SOL).
|
|
622
|
+
|
|
623
|
+
---
|
|
624
|
+
|
|
625
|
+
## AI Agent Mode ā autonomous browser automation
|
|
626
|
+
|
|
627
|
+
Give a task in natural language ā the agent drives the browser autonomously until it's done.
|
|
628
|
+
|
|
629
|
+
### Quick Start
|
|
630
|
+
|
|
631
|
+
```js
|
|
632
|
+
const { runAgent } = require('./.agents/skills/human-browser/scripts/browser-agent');
|
|
633
|
+
|
|
634
|
+
const result = await runAgent({
|
|
635
|
+
task: 'Go to reddit.com/r/programming and find the top post title',
|
|
636
|
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
637
|
+
provider: 'anthropic', // or 'openai', 'openrouter'
|
|
638
|
+
model: 'claude-sonnet-4-6',
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
console.log(result.output); // "The top post is: ..."
|
|
642
|
+
console.log(result.steps); // 3
|
|
643
|
+
console.log(result.success); // true
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
### CLI
|
|
647
|
+
|
|
648
|
+
```bash
|
|
649
|
+
export AGENT_LLM_API_KEY=sk-...
|
|
650
|
+
export AGENT_LLM_PROVIDER=openrouter # anthropic | openai | openrouter
|
|
651
|
+
export AGENT_LLM_MODEL=anthropic/claude-sonnet-4-6
|
|
652
|
+
|
|
653
|
+
node browser-agent.js "Search Google for 'best AI tools 2026' and list the top 3 results"
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
### How it works
|
|
657
|
+
|
|
658
|
+
1. **Snapshot** ā extracts all interactive elements (links, buttons, inputs) + visible text from the DOM
|
|
659
|
+
2. **LLM decides** ā sends the snapshot to Claude/GPT ā gets back structured actions (click, type, scroll, navigate)
|
|
660
|
+
3. **Execute** ā performs the actions on the stealth browser with human-like behavior (Bezier mouse, variable typing speed)
|
|
661
|
+
4. **Repeat** ā takes a new snapshot and loops until the agent says "done" or hits max steps
|
|
662
|
+
|
|
663
|
+
### Options
|
|
664
|
+
|
|
665
|
+
```js
|
|
666
|
+
await runAgent({
|
|
667
|
+
task: '...', // Required: natural language task
|
|
668
|
+
provider: 'anthropic', // LLM provider
|
|
669
|
+
model: 'claude-sonnet-4-6', // Model name
|
|
670
|
+
apiKey: 'sk-...', // API key
|
|
671
|
+
startUrl: 'https://...', // Navigate here before starting
|
|
672
|
+
maxSteps: 30, // Max loop iterations (default: 30)
|
|
673
|
+
verbose: true, // Detailed logging
|
|
674
|
+
country: 'us', // Proxy country
|
|
675
|
+
mobile: true, // iPhone or Desktop
|
|
676
|
+
useProxy: true, // Use residential proxy
|
|
677
|
+
headless: true, // Headless mode
|
|
678
|
+
onStep: (step, actions, snap) => { ... }, // Step callback
|
|
679
|
+
});
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
### Env vars
|
|
683
|
+
|
|
684
|
+
| Variable | Description | Default |
|
|
685
|
+
|----------|-------------|---------|
|
|
686
|
+
| `AGENT_LLM_PROVIDER` | anthropic, openai, openrouter | anthropic |
|
|
687
|
+
| `AGENT_LLM_MODEL` | Model name | claude-sonnet-4-6 |
|
|
688
|
+
| `AGENT_LLM_API_KEY` | API key for the LLM | ā |
|
|
689
|
+
| `AGENT_MAX_STEPS` | Max iterations | 30 |
|
|
690
|
+
| `AGENT_VERBOSE` | Set to "1" for detailed logs | ā |
|
|
691
|
+
|
|
692
|
+
All `HB_PROXY_*` env vars from launchHuman() also apply ā the agent uses the same stealth browser under the hood.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "human-browser",
|
|
3
|
-
"version": "4.
|
|
4
|
-
"description": "Stealth browser for AI agents. Bypasses Cloudflare, DataDome, PerimeterX. Residential IPs from 10+ countries. iPhone 15 Pro fingerprint. Drop-in Playwright replacement
|
|
3
|
+
"version": "4.1.0",
|
|
4
|
+
"description": "Stealth browser for AI agents. Bypasses Cloudflare, DataDome, PerimeterX. Residential IPs from 10+ countries. iPhone 15 Pro fingerprint. Drop-in Playwright replacement \u2014 launchHuman() just works.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"browser-automation",
|
|
7
7
|
"stealth-browser",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
],
|
|
32
32
|
"author": "al1enjesus",
|
|
33
33
|
"license": "MIT",
|
|
34
|
-
"homepage": "https://humanbrowser.
|
|
34
|
+
"homepage": "https://humanbrowser.cloud",
|
|
35
35
|
"repository": {
|
|
36
36
|
"type": "git",
|
|
37
37
|
"url": "https://github.com/al1enjesus/human-browser"
|
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* browser-agent.js ā AI Agent Layer for Human Browser v1.0.0
|
|
3
|
+
*
|
|
4
|
+
* Give a task in natural language ā agent drives the browser autonomously.
|
|
5
|
+
* Built on top of launchHuman() stealth browser with residential proxies.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const { runAgent } = require('./browser-agent');
|
|
9
|
+
* const result = await runAgent({
|
|
10
|
+
* task: 'Go to reddit.com and find the top post on r/programming',
|
|
11
|
+
* model: 'claude-sonnet-4-6', // any OpenRouter/Anthropic/OpenAI model
|
|
12
|
+
* apiKey: process.env.ANTHROPIC_API_KEY,
|
|
13
|
+
* provider: 'anthropic', // 'anthropic' | 'openai' | 'openrouter'
|
|
14
|
+
* });
|
|
15
|
+
* console.log(result.output);
|
|
16
|
+
*
|
|
17
|
+
* Env vars:
|
|
18
|
+
* AGENT_LLM_PROVIDER ā anthropic | openai | openrouter (default: anthropic)
|
|
19
|
+
* AGENT_LLM_MODEL ā model name (default: claude-sonnet-4-6)
|
|
20
|
+
* AGENT_LLM_API_KEY ā API key for the LLM provider
|
|
21
|
+
* AGENT_MAX_STEPS ā max agent loop iterations (default: 30)
|
|
22
|
+
* AGENT_VERBOSE ā set to '1' for detailed logging
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const { launchHuman, getTrial, humanClick, humanType, humanScroll, humanRead, sleep, rand } = require('./browser-human');
|
|
26
|
+
|
|
27
|
+
// āāā LLM PROVIDERS āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
28
|
+
|
|
29
|
+
const PROVIDERS = {
|
|
30
|
+
anthropic: {
|
|
31
|
+
url: 'https://api.anthropic.com/v1/messages',
|
|
32
|
+
headers: (key) => ({
|
|
33
|
+
'x-api-key': key,
|
|
34
|
+
'anthropic-version': '2023-06-01',
|
|
35
|
+
'content-type': 'application/json',
|
|
36
|
+
}),
|
|
37
|
+
buildBody: (model, messages, systemPrompt) => ({
|
|
38
|
+
model,
|
|
39
|
+
max_tokens: 4096,
|
|
40
|
+
system: systemPrompt,
|
|
41
|
+
messages,
|
|
42
|
+
}),
|
|
43
|
+
parseResponse: (data) => {
|
|
44
|
+
const block = data.content?.find(b => b.type === 'text');
|
|
45
|
+
return block?.text || '';
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
openai: {
|
|
49
|
+
url: 'https://api.openai.com/v1/chat/completions',
|
|
50
|
+
headers: (key) => ({
|
|
51
|
+
'Authorization': `Bearer ${key}`,
|
|
52
|
+
'content-type': 'application/json',
|
|
53
|
+
}),
|
|
54
|
+
buildBody: (model, messages, systemPrompt) => ({
|
|
55
|
+
model,
|
|
56
|
+
max_tokens: 4096,
|
|
57
|
+
messages: [{ role: 'system', content: systemPrompt }, ...messages],
|
|
58
|
+
}),
|
|
59
|
+
parseResponse: (data) => data.choices?.[0]?.message?.content || '',
|
|
60
|
+
},
|
|
61
|
+
openrouter: {
|
|
62
|
+
url: 'https://openrouter.ai/api/v1/chat/completions',
|
|
63
|
+
headers: (key) => ({
|
|
64
|
+
'Authorization': `Bearer ${key}`,
|
|
65
|
+
'content-type': 'application/json',
|
|
66
|
+
}),
|
|
67
|
+
buildBody: (model, messages, systemPrompt) => ({
|
|
68
|
+
model,
|
|
69
|
+
max_tokens: 4096,
|
|
70
|
+
messages: [{ role: 'system', content: systemPrompt }, ...messages],
|
|
71
|
+
}),
|
|
72
|
+
parseResponse: (data) => data.choices?.[0]?.message?.content || '',
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
async function callLLM(provider, apiKey, model, messages, systemPrompt) {
|
|
77
|
+
const p = PROVIDERS[provider];
|
|
78
|
+
if (!p) throw new Error(`Unknown provider: ${provider}. Use: anthropic, openai, openrouter`);
|
|
79
|
+
|
|
80
|
+
const resp = await fetch(p.url, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: p.headers(apiKey),
|
|
83
|
+
body: JSON.stringify(p.buildBody(model, messages, systemPrompt)),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (!resp.ok) {
|
|
87
|
+
const errText = await resp.text();
|
|
88
|
+
throw new Error(`LLM API error ${resp.status}: ${errText.slice(0, 500)}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const data = await resp.json();
|
|
92
|
+
return p.parseResponse(data);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// āāā PAGE SNAPSHOT āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Extract a compact, LLM-friendly representation of the visible page.
|
|
99
|
+
* Returns interactive elements with ref IDs for the agent to use.
|
|
100
|
+
*/
|
|
101
|
+
async function getPageSnapshot(page) {
|
|
102
|
+
const snapshot = await page.evaluate(() => {
|
|
103
|
+
const result = {
|
|
104
|
+
url: location.href,
|
|
105
|
+
title: document.title || '',
|
|
106
|
+
viewport: { width: window.innerWidth || 0, height: window.innerHeight || 0 },
|
|
107
|
+
scrollY: window.scrollY || 0,
|
|
108
|
+
scrollHeight: (document.documentElement || {}).scrollHeight || 0,
|
|
109
|
+
elements: [],
|
|
110
|
+
visibleText: '',
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const body = document.body || document.documentElement;
|
|
114
|
+
if (!body) return result;
|
|
115
|
+
|
|
116
|
+
const elements = [];
|
|
117
|
+
let refId = 0;
|
|
118
|
+
|
|
119
|
+
function isVisible(el) {
|
|
120
|
+
try {
|
|
121
|
+
const style = window.getComputedStyle(el);
|
|
122
|
+
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
|
|
123
|
+
const rect = el.getBoundingClientRect();
|
|
124
|
+
return rect.width > 0 && rect.height > 0 && rect.top < window.innerHeight && rect.bottom > 0;
|
|
125
|
+
} catch { return false; }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function getLabel(el) {
|
|
129
|
+
return (
|
|
130
|
+
el.getAttribute('aria-label') ||
|
|
131
|
+
el.getAttribute('placeholder') ||
|
|
132
|
+
el.getAttribute('title') ||
|
|
133
|
+
el.getAttribute('alt') ||
|
|
134
|
+
el.getAttribute('name') ||
|
|
135
|
+
''
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function collect(root) {
|
|
140
|
+
try {
|
|
141
|
+
const selectors = 'a, button, input, textarea, select, [role="button"], [role="link"], [role="tab"], [role="menuitem"], [contenteditable="true"], [onclick]';
|
|
142
|
+
for (const el of root.querySelectorAll(selectors)) {
|
|
143
|
+
if (!isVisible(el)) continue;
|
|
144
|
+
const rect = el.getBoundingClientRect();
|
|
145
|
+
const tag = el.tagName.toLowerCase();
|
|
146
|
+
const text = (el.textContent || '').trim().slice(0, 80);
|
|
147
|
+
const label = getLabel(el);
|
|
148
|
+
const type = el.getAttribute('type') || '';
|
|
149
|
+
const href = el.getAttribute('href') || '';
|
|
150
|
+
const value = el.value || '';
|
|
151
|
+
const ref = `e${refId++}`;
|
|
152
|
+
|
|
153
|
+
const info = { ref, tag, x: Math.round(rect.x + rect.width / 2), y: Math.round(rect.y + rect.height / 2) };
|
|
154
|
+
if (text) info.text = text;
|
|
155
|
+
if (label) info.label = label;
|
|
156
|
+
if (type) info.type = type;
|
|
157
|
+
if (href) info.href = href.slice(0, 120);
|
|
158
|
+
if (value) info.value = value.slice(0, 60);
|
|
159
|
+
if (el.disabled) info.disabled = true;
|
|
160
|
+
if (el.checked) info.checked = true;
|
|
161
|
+
|
|
162
|
+
elements.push(info);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Recurse into shadow DOMs
|
|
166
|
+
for (const n of root.querySelectorAll('*')) {
|
|
167
|
+
if (n.shadowRoot) collect(n.shadowRoot);
|
|
168
|
+
}
|
|
169
|
+
} catch {}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
collect(body);
|
|
173
|
+
result.elements = elements;
|
|
174
|
+
|
|
175
|
+
// Get visible text blocks
|
|
176
|
+
try {
|
|
177
|
+
const textBlocks = [];
|
|
178
|
+
const walker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, {
|
|
179
|
+
acceptNode: (node) => {
|
|
180
|
+
const t = node.textContent.trim();
|
|
181
|
+
if (t.length < 10) return NodeFilter.FILTER_REJECT;
|
|
182
|
+
const parent = node.parentElement;
|
|
183
|
+
if (!parent) return NodeFilter.FILTER_REJECT;
|
|
184
|
+
const tag = parent.tagName.toLowerCase();
|
|
185
|
+
if (['script', 'style', 'noscript'].includes(tag)) return NodeFilter.FILTER_REJECT;
|
|
186
|
+
const rect = parent.getBoundingClientRect();
|
|
187
|
+
if (rect.width === 0 || rect.height === 0) return NodeFilter.FILTER_REJECT;
|
|
188
|
+
if (rect.top > window.innerHeight * 2) return NodeFilter.FILTER_REJECT;
|
|
189
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
let node;
|
|
194
|
+
let charBudget = 3000;
|
|
195
|
+
while ((node = walker.nextNode()) && charBudget > 0) {
|
|
196
|
+
const t = node.textContent.trim().slice(0, 200);
|
|
197
|
+
textBlocks.push(t);
|
|
198
|
+
charBudget -= t.length;
|
|
199
|
+
}
|
|
200
|
+
result.visibleText = textBlocks.join('\n');
|
|
201
|
+
} catch {}
|
|
202
|
+
|
|
203
|
+
return result;
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
return snapshot;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Format snapshot into a concise string for the LLM
|
|
211
|
+
*/
|
|
212
|
+
function formatSnapshot(snap) {
|
|
213
|
+
const lines = [];
|
|
214
|
+
lines.push(`## Page: ${snap.title}`);
|
|
215
|
+
lines.push(`URL: ${snap.url}`);
|
|
216
|
+
lines.push(`Scroll: ${snap.scrollY}/${snap.scrollHeight - snap.viewport.height}px`);
|
|
217
|
+
lines.push('');
|
|
218
|
+
|
|
219
|
+
if (snap.visibleText) {
|
|
220
|
+
lines.push('### Visible text:');
|
|
221
|
+
lines.push(snap.visibleText.slice(0, 2000));
|
|
222
|
+
lines.push('');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (snap.elements.length > 0) {
|
|
226
|
+
lines.push(`### Interactive elements (${snap.elements.length}):`);
|
|
227
|
+
for (const el of snap.elements) {
|
|
228
|
+
let desc = `[${el.ref}] <${el.tag}>`;
|
|
229
|
+
if (el.type) desc += ` type="${el.type}"`;
|
|
230
|
+
if (el.text) desc += ` "${el.text}"`;
|
|
231
|
+
if (el.label) desc += ` label="${el.label}"`;
|
|
232
|
+
if (el.href) desc += ` href="${el.href}"`;
|
|
233
|
+
if (el.value) desc += ` value="${el.value}"`;
|
|
234
|
+
if (el.disabled) desc += ' [disabled]';
|
|
235
|
+
if (el.checked) desc += ' [checked]';
|
|
236
|
+
desc += ` @(${el.x},${el.y})`;
|
|
237
|
+
lines.push(desc);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return lines.join('\n');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// āāā AGENT ACTIONS āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Parse the LLM response into structured actions.
|
|
248
|
+
* The LLM outputs JSON actions in a ```json block.
|
|
249
|
+
*/
|
|
250
|
+
function parseActions(llmOutput) {
|
|
251
|
+
// Extract JSON block
|
|
252
|
+
const jsonMatch = llmOutput.match(/```json\s*([\s\S]*?)```/);
|
|
253
|
+
if (!jsonMatch) {
|
|
254
|
+
// Try to parse the whole output as JSON
|
|
255
|
+
try {
|
|
256
|
+
const parsed = JSON.parse(llmOutput.trim());
|
|
257
|
+
return Array.isArray(parsed) ? parsed : [parsed];
|
|
258
|
+
} catch {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const parsed = JSON.parse(jsonMatch[1].trim());
|
|
265
|
+
return Array.isArray(parsed) ? parsed : [parsed];
|
|
266
|
+
} catch {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Execute a single action on the page
|
|
273
|
+
*/
|
|
274
|
+
async function executeAction(page, action, elements) {
|
|
275
|
+
const log = (...a) => console.log('[agent]', ...a);
|
|
276
|
+
|
|
277
|
+
switch (action.action) {
|
|
278
|
+
case 'click': {
|
|
279
|
+
const el = elements.find(e => e.ref === action.ref);
|
|
280
|
+
if (!el) throw new Error(`Element ${action.ref} not found`);
|
|
281
|
+
log(`click ${action.ref} "${el.text || el.label || ''}" @(${el.x},${el.y})`);
|
|
282
|
+
await humanClick(page, el.x, el.y);
|
|
283
|
+
await sleep(rand(500, 1500));
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
case 'type': {
|
|
288
|
+
const el = elements.find(e => e.ref === action.ref);
|
|
289
|
+
if (!el) throw new Error(`Element ${action.ref} not found`);
|
|
290
|
+
log(`type into ${action.ref} "${action.text?.slice(0, 30)}..."`);
|
|
291
|
+
// Click first, clear, then type
|
|
292
|
+
await humanClick(page, el.x, el.y);
|
|
293
|
+
await sleep(200);
|
|
294
|
+
if (action.clear !== false) {
|
|
295
|
+
await page.keyboard.press('Control+a');
|
|
296
|
+
await sleep(100);
|
|
297
|
+
}
|
|
298
|
+
await humanType(page, `[data-agent-ref="${action.ref}"]`, action.text || '').catch(async () => {
|
|
299
|
+
// Fallback: type character by character at coordinates
|
|
300
|
+
for (const char of (action.text || '')) {
|
|
301
|
+
await page.keyboard.type(char);
|
|
302
|
+
await sleep(rand(60, 180));
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
await sleep(rand(300, 600));
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
case 'press': {
|
|
310
|
+
log(`press key: ${action.key}`);
|
|
311
|
+
await page.keyboard.press(action.key);
|
|
312
|
+
await sleep(rand(200, 500));
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
case 'scroll': {
|
|
317
|
+
const dir = action.direction || 'down';
|
|
318
|
+
const amount = action.amount || rand(300, 600);
|
|
319
|
+
log(`scroll ${dir} ${amount}px`);
|
|
320
|
+
await humanScroll(page, dir, amount);
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
case 'navigate': {
|
|
325
|
+
log(`navigate to: ${action.url}`);
|
|
326
|
+
try {
|
|
327
|
+
await page.goto(action.url, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
|
328
|
+
} catch (e) {
|
|
329
|
+
// If domcontentloaded times out, page may still be usable
|
|
330
|
+
if (e.message.includes('Timeout')) {
|
|
331
|
+
log(`Navigation timeout, page may still be usable`);
|
|
332
|
+
} else {
|
|
333
|
+
throw e;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
await sleep(rand(1000, 2000));
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
case 'wait': {
|
|
341
|
+
const ms = action.ms || 2000;
|
|
342
|
+
log(`wait ${ms}ms`);
|
|
343
|
+
await sleep(ms);
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
case 'screenshot': {
|
|
348
|
+
log('taking screenshot');
|
|
349
|
+
// Screenshot is handled by the caller if vision is supported
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
case 'extract': {
|
|
354
|
+
log(`extract: ${action.selector || 'page text'}`);
|
|
355
|
+
if (action.selector) {
|
|
356
|
+
const el = await page.$(action.selector);
|
|
357
|
+
return el ? await el.textContent() : null;
|
|
358
|
+
}
|
|
359
|
+
return await page.evaluate(() => (document.body || document.documentElement)?.innerText?.slice(0, 5000) || '');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
case 'done': {
|
|
363
|
+
log(`task complete: ${action.result?.slice(0, 100)}`);
|
|
364
|
+
return { done: true, result: action.result || '' };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
case 'fail': {
|
|
368
|
+
log(`task failed: ${action.reason}`);
|
|
369
|
+
return { done: true, failed: true, result: action.reason || 'Unknown error' };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
default:
|
|
373
|
+
log(`unknown action: ${action.action}`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// āāā SYSTEM PROMPT āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
380
|
+
|
|
381
|
+
const SYSTEM_PROMPT = `You are a browser automation agent. You control a real browser with a residential IP and stealth fingerprint. You see a snapshot of the current page and must decide what actions to take.
|
|
382
|
+
|
|
383
|
+
## Output format
|
|
384
|
+
|
|
385
|
+
Respond with a brief thought (1-2 sentences), then a JSON action block:
|
|
386
|
+
|
|
387
|
+
\`\`\`json
|
|
388
|
+
[{"action": "click", "ref": "e5"}]
|
|
389
|
+
\`\`\`
|
|
390
|
+
|
|
391
|
+
## Available actions
|
|
392
|
+
|
|
393
|
+
- **click** ā Click an element: \`{"action": "click", "ref": "e12"}\`
|
|
394
|
+
- **type** ā Type text into an input: \`{"action": "type", "ref": "e3", "text": "hello"}\`
|
|
395
|
+
- Add \`"clear": false\` to append instead of replacing
|
|
396
|
+
- **press** ā Press a key: \`{"action": "press", "key": "Enter"}\`
|
|
397
|
+
- Keys: Enter, Tab, Escape, ArrowDown, ArrowUp, Backspace, etc.
|
|
398
|
+
- **scroll** ā Scroll the page: \`{"action": "scroll", "direction": "down"}\`
|
|
399
|
+
- direction: "down" or "up", optional "amount": pixels
|
|
400
|
+
- **navigate** ā Go to a URL: \`{"action": "navigate", "url": "https://..."}\`
|
|
401
|
+
- **wait** ā Wait for content to load: \`{"action": "wait", "ms": 2000}\`
|
|
402
|
+
- **extract** ā Extract text: \`{"action": "extract"}\` or \`{"action": "extract", "selector": ".content"}\`
|
|
403
|
+
- **done** ā Task complete: \`{"action": "done", "result": "The answer is..."}\`
|
|
404
|
+
- **fail** ā Cannot complete: \`{"action": "fail", "reason": "Why it failed"}\`
|
|
405
|
+
|
|
406
|
+
## Rules
|
|
407
|
+
|
|
408
|
+
1. Use element refs like "e5" from the snapshot ā they correspond to interactive elements.
|
|
409
|
+
2. You can chain multiple actions: \`[{"action": "click", "ref": "e3"}, {"action": "type", "ref": "e3", "text": "query"}]\`
|
|
410
|
+
3. After clicking a link or button, the page may change. Wait for the next snapshot.
|
|
411
|
+
4. If a page requires scrolling to find content, scroll down first.
|
|
412
|
+
5. When the task is complete, ALWAYS use the "done" action with the result.
|
|
413
|
+
6. If stuck after 3+ attempts, use "fail" with a clear reason.
|
|
414
|
+
7. Keep thoughts SHORT. Focus on actions.
|
|
415
|
+
8. Don't hallucinate elements ā only use refs from the current snapshot.
|
|
416
|
+
9. For search: navigate to the search engine, type the query, press Enter.
|
|
417
|
+
10. Cookie banners and popups: dismiss them (click accept/close) and continue.`;
|
|
418
|
+
|
|
419
|
+
// āāā MAIN AGENT LOOP āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Run an AI agent that controls the browser to complete a task.
|
|
423
|
+
*
|
|
424
|
+
* @param {Object} opts
|
|
425
|
+
* @param {string} opts.task ā Natural language task description
|
|
426
|
+
* @param {string} opts.provider ā LLM provider: anthropic|openai|openrouter (default: env or anthropic)
|
|
427
|
+
* @param {string} opts.model ā Model name (default: env or claude-sonnet-4-6)
|
|
428
|
+
* @param {string} opts.apiKey ā LLM API key (default: env)
|
|
429
|
+
* @param {string} opts.startUrl ā Starting URL (default: about:blank)
|
|
430
|
+
* @param {number} opts.maxSteps ā Max agent loop iterations (default: 30)
|
|
431
|
+
* @param {boolean} opts.verbose ā Detailed logging (default: env or false)
|
|
432
|
+
* @param {string} opts.country ā Proxy country (default: ro)
|
|
433
|
+
* @param {boolean} opts.mobile ā Mobile device (default: true)
|
|
434
|
+
* @param {boolean} opts.useProxy ā Use residential proxy (default: true)
|
|
435
|
+
* @param {boolean} opts.headless ā Headless mode (default: true)
|
|
436
|
+
* @param {Function} opts.onStep ā Callback after each step: (stepNum, action, snapshot) => void
|
|
437
|
+
* @param {Object} opts.browserOpts ā Extra options for launchHuman()
|
|
438
|
+
*
|
|
439
|
+
* @returns {{ output: string, steps: number, success: boolean, history: Array }}
|
|
440
|
+
*/
|
|
441
|
+
async function runAgent(opts = {}) {
|
|
442
|
+
const {
|
|
443
|
+
task,
|
|
444
|
+
provider = process.env.AGENT_LLM_PROVIDER || 'anthropic',
|
|
445
|
+
model = process.env.AGENT_LLM_MODEL || 'claude-sonnet-4-6',
|
|
446
|
+
apiKey = process.env.AGENT_LLM_API_KEY || process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY,
|
|
447
|
+
startUrl = null,
|
|
448
|
+
maxSteps = parseInt(process.env.AGENT_MAX_STEPS || '30'),
|
|
449
|
+
verbose = process.env.AGENT_VERBOSE === '1',
|
|
450
|
+
country = 'ro',
|
|
451
|
+
mobile = true,
|
|
452
|
+
useProxy = true,
|
|
453
|
+
headless = true,
|
|
454
|
+
onStep = null,
|
|
455
|
+
browserOpts = {},
|
|
456
|
+
} = opts;
|
|
457
|
+
|
|
458
|
+
if (!task) throw new Error('task is required');
|
|
459
|
+
if (!apiKey) throw new Error('API key is required. Set AGENT_LLM_API_KEY or pass opts.apiKey');
|
|
460
|
+
|
|
461
|
+
const log = (...a) => console.log('[browser-agent]', ...a);
|
|
462
|
+
const vlog = verbose ? log : () => {};
|
|
463
|
+
|
|
464
|
+
log(`Task: "${task.slice(0, 100)}"`);
|
|
465
|
+
log(`Model: ${provider}/${model} | Max steps: ${maxSteps}`);
|
|
466
|
+
|
|
467
|
+
// Launch browser
|
|
468
|
+
const { browser, page, ctx } = await launchHuman({
|
|
469
|
+
country,
|
|
470
|
+
mobile,
|
|
471
|
+
useProxy,
|
|
472
|
+
headless,
|
|
473
|
+
...browserOpts,
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
const messages = [];
|
|
477
|
+
const history = [];
|
|
478
|
+
let result = { output: '', steps: 0, success: false, history };
|
|
479
|
+
|
|
480
|
+
try {
|
|
481
|
+
// Navigate to start URL if provided
|
|
482
|
+
if (startUrl) {
|
|
483
|
+
try {
|
|
484
|
+
await page.goto(startUrl, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
|
485
|
+
} catch (e) {
|
|
486
|
+
if (!e.message.includes('Timeout')) throw e;
|
|
487
|
+
log('Start URL navigation timeout, continuing...');
|
|
488
|
+
}
|
|
489
|
+
await sleep(rand(1000, 2000));
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
for (let step = 0; step < maxSteps; step++) {
|
|
493
|
+
vlog(`--- Step ${step + 1}/${maxSteps} ---`);
|
|
494
|
+
|
|
495
|
+
// Get page snapshot
|
|
496
|
+
let snapshot;
|
|
497
|
+
try {
|
|
498
|
+
snapshot = await getPageSnapshot(page);
|
|
499
|
+
} catch (e) {
|
|
500
|
+
vlog(`Snapshot error: ${e.message}`);
|
|
501
|
+
await sleep(1000);
|
|
502
|
+
try { snapshot = await getPageSnapshot(page); } catch { snapshot = { url: page.url(), title: '', elements: [], visibleText: '', scrollY: 0, scrollHeight: 0, viewport: { width: 0, height: 0 } }; }
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const snapshotStr = formatSnapshot(snapshot);
|
|
506
|
+
vlog(`Page: ${snapshot.url} | Elements: ${snapshot.elements.length}`);
|
|
507
|
+
|
|
508
|
+
// Build user message
|
|
509
|
+
const userMsg = step === 0
|
|
510
|
+
? `Task: ${task}\n\nCurrent page:\n${snapshotStr}`
|
|
511
|
+
: `Page after action:\n${snapshotStr}`;
|
|
512
|
+
|
|
513
|
+
messages.push({ role: 'user', content: userMsg });
|
|
514
|
+
|
|
515
|
+
// Call LLM
|
|
516
|
+
let llmResponse;
|
|
517
|
+
try {
|
|
518
|
+
llmResponse = await callLLM(provider, apiKey, model, messages, SYSTEM_PROMPT);
|
|
519
|
+
} catch (e) {
|
|
520
|
+
log(`LLM error: ${e.message}`);
|
|
521
|
+
result.output = `LLM error: ${e.message}`;
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
vlog(`LLM response: ${llmResponse.slice(0, 200)}...`);
|
|
526
|
+
messages.push({ role: 'assistant', content: llmResponse });
|
|
527
|
+
|
|
528
|
+
// Parse and execute actions
|
|
529
|
+
const actions = parseActions(llmResponse);
|
|
530
|
+
if (!actions || actions.length === 0) {
|
|
531
|
+
log(`No valid actions in LLM response, retrying...`);
|
|
532
|
+
messages.push({ role: 'user', content: 'Your response did not contain valid JSON actions. Please respond with actions in ```json [...] ``` format.' });
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
let stepDone = false;
|
|
537
|
+
for (const action of actions) {
|
|
538
|
+
try {
|
|
539
|
+
const actionResult = await executeAction(page, action, snapshot.elements);
|
|
540
|
+
history.push({ step: step + 1, action, success: true });
|
|
541
|
+
|
|
542
|
+
if (actionResult?.done) {
|
|
543
|
+
result.output = actionResult.result;
|
|
544
|
+
result.success = !actionResult.failed;
|
|
545
|
+
result.steps = step + 1;
|
|
546
|
+
stepDone = true;
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
} catch (e) {
|
|
550
|
+
log(`Action error: ${e.message}`);
|
|
551
|
+
history.push({ step: step + 1, action, success: false, error: e.message });
|
|
552
|
+
messages.push({ role: 'user', content: `Action "${action.action}" failed: ${e.message}. Try a different approach.` });
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (stepDone) break;
|
|
557
|
+
if (onStep) onStep(step + 1, actions, snapshot);
|
|
558
|
+
|
|
559
|
+
// Small delay between steps
|
|
560
|
+
await sleep(rand(500, 1000));
|
|
561
|
+
result.steps = step + 1;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (!result.output && result.steps >= maxSteps) {
|
|
565
|
+
result.output = 'Max steps reached without completing the task.';
|
|
566
|
+
result.success = false;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
} finally {
|
|
570
|
+
await browser.close().catch(() => {});
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
log(`Done in ${result.steps} steps. Success: ${result.success}`);
|
|
574
|
+
return result;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// āāā EXPORTS āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
578
|
+
|
|
579
|
+
module.exports = {
|
|
580
|
+
runAgent,
|
|
581
|
+
getPageSnapshot,
|
|
582
|
+
formatSnapshot,
|
|
583
|
+
callLLM,
|
|
584
|
+
PROVIDERS,
|
|
585
|
+
SYSTEM_PROMPT,
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
// āāā CLI āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
589
|
+
|
|
590
|
+
if (require.main === module) {
|
|
591
|
+
const task = process.argv.slice(2).join(' ');
|
|
592
|
+
if (!task) {
|
|
593
|
+
console.log('Usage: node browser-agent.js <task>');
|
|
594
|
+
console.log(' Example: node browser-agent.js "Search Google for OpenAI news and give me the top 3 results"');
|
|
595
|
+
console.log('');
|
|
596
|
+
console.log('Env vars:');
|
|
597
|
+
console.log(' AGENT_LLM_API_KEY ā API key (required)');
|
|
598
|
+
console.log(' AGENT_LLM_PROVIDER ā anthropic | openai | openrouter');
|
|
599
|
+
console.log(' AGENT_LLM_MODEL ā model name');
|
|
600
|
+
console.log(' AGENT_MAX_STEPS ā max iterations (default: 30)');
|
|
601
|
+
console.log(' AGENT_VERBOSE ā 1 for detailed logs');
|
|
602
|
+
process.exit(1);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
(async () => {
|
|
606
|
+
try {
|
|
607
|
+
const result = await runAgent({ task, verbose: true });
|
|
608
|
+
console.log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
609
|
+
console.log(`Result (${result.steps} steps, success=${result.success}):`);
|
|
610
|
+
console.log(result.output);
|
|
611
|
+
} catch (e) {
|
|
612
|
+
console.error('Agent error:', e.message);
|
|
613
|
+
process.exit(1);
|
|
614
|
+
}
|
|
615
|
+
})();
|
|
616
|
+
}
|
package/scripts/browser-human.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Appears as iPhone 15 Pro or Desktop Chrome to every website.
|
|
6
6
|
* Bypasses Cloudflare, DataDome, PerimeterX out of the box.
|
|
7
7
|
*
|
|
8
|
-
* Get credentials: https://humanbrowser.
|
|
8
|
+
* Get credentials: https://humanbrowser.cloud
|
|
9
9
|
* Support: https://t.me/virixlabs
|
|
10
10
|
*
|
|
11
11
|
* Usage:
|
|
@@ -200,7 +200,7 @@ function makeProxy(sessionId = null, country = null) {
|
|
|
200
200
|
// āāā TRIAL CREDENTIALS āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
201
201
|
|
|
202
202
|
/**
|
|
203
|
-
* Get free trial credentials from humanbrowser.
|
|
203
|
+
* Get free trial credentials from humanbrowser.cloud
|
|
204
204
|
* Sets HB_PROXY_USER, HB_PROXY_PASS, HB_PROXY_SESSION, HB_PROXY_PROVIDER
|
|
205
205
|
* No signup needed ā Romania residential proxy
|
|
206
206
|
*
|
|
@@ -216,7 +216,7 @@ async function getTrial() {
|
|
|
216
216
|
try {
|
|
217
217
|
const https = require('https');
|
|
218
218
|
const data = await new Promise((resolve, reject) => {
|
|
219
|
-
const req = https.get('https://humanbrowser.
|
|
219
|
+
const req = https.get('https://humanbrowser.cloud/api/trial', res => {
|
|
220
220
|
let body = '';
|
|
221
221
|
res.on('data', d => body += d);
|
|
222
222
|
res.on('end', () => { try { resolve(JSON.parse(body)); } catch (e) { reject(e); } });
|
|
@@ -239,7 +239,7 @@ async function getTrial() {
|
|
|
239
239
|
if (!process.env.HB_PROXY_COUNTRY) process.env.HB_PROXY_COUNTRY = country;
|
|
240
240
|
|
|
241
241
|
console.log(`š Human Browser trial activated! (~100MB Romania residential IP)`);
|
|
242
|
-
console.log(` Upgrade at: https://humanbrowser.
|
|
242
|
+
console.log(` Upgrade at: https://humanbrowser.cloud\n`);
|
|
243
243
|
return { ok: true, provider, country, session };
|
|
244
244
|
}
|
|
245
245
|
|
|
@@ -247,9 +247,9 @@ async function getTrial() {
|
|
|
247
247
|
} catch (err) {
|
|
248
248
|
const e = new Error(err.message);
|
|
249
249
|
e.code = 'TRIAL_UNAVAILABLE';
|
|
250
|
-
e.cta_url = 'https://humanbrowser.
|
|
250
|
+
e.cta_url = 'https://humanbrowser.cloud';
|
|
251
251
|
console.warn('[human-browser] Trial fetch failed:', err.message);
|
|
252
|
-
console.warn(' ā Get credentials at: https://humanbrowser.
|
|
252
|
+
console.warn(' ā Get credentials at: https://humanbrowser.cloud');
|
|
253
253
|
throw e;
|
|
254
254
|
}
|
|
255
255
|
}
|
|
@@ -467,7 +467,7 @@ async function launchHuman(opts = {}) {
|
|
|
467
467
|
await getTrial();
|
|
468
468
|
} catch (e) {
|
|
469
469
|
console.warn('ā ļø Could not fetch trial credentials:', e.message);
|
|
470
|
-
console.warn(' Get credentials at: https://humanbrowser.
|
|
470
|
+
console.warn(' Get credentials at: https://humanbrowser.cloud');
|
|
471
471
|
}
|
|
472
472
|
}
|
|
473
473
|
|