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 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.dev
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.dev
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.dev, then set `PROXY_USER` / `PROXY_PASS` in your `.env`.
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.dev** — we handle everything, from $13.99/mo.
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.dev
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.dev** — get credentials, manage subscription
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.0.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 — launchHuman() just works.",
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.dev",
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
+ }
@@ -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.dev
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.dev
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.dev/api/trial', res => {
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.dev\n`);
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.dev';
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.dev');
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.dev');
470
+ console.warn(' Get credentials at: https://humanbrowser.cloud');
471
471
  }
472
472
  }
473
473