@agentium/browser 1.0.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/dist/index.cjs ADDED
@@ -0,0 +1,1026 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ BrowserAgent: () => BrowserAgent,
34
+ BrowserProvider: () => BrowserProvider,
35
+ CredentialVault: () => CredentialVault
36
+ });
37
+ module.exports = __toCommonJS(index_exports);
38
+
39
+ // src/browser-agent.ts
40
+ var import_core = require("@agentium/core");
41
+ var import_zod = require("zod");
42
+
43
+ // src/stealth.ts
44
+ var REALISTIC_USER_AGENTS = [
45
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
46
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
47
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
48
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
49
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
50
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.2 Safari/605.1.15"
51
+ ];
52
+ function pickUserAgent() {
53
+ return REALISTIC_USER_AGENTS[Math.floor(Math.random() * REALISTIC_USER_AGENTS.length)];
54
+ }
55
+ function getStealthScript() {
56
+ return `
57
+ // \u2500\u2500 navigator.webdriver \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
58
+ Object.defineProperty(navigator, 'webdriver', {
59
+ get: () => undefined,
60
+ configurable: true,
61
+ });
62
+
63
+ // \u2500\u2500 navigator.plugins \u2014 appear non-empty \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
64
+ Object.defineProperty(navigator, 'plugins', {
65
+ get: () => {
66
+ const plugins = [
67
+ { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
68
+ { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '' },
69
+ { name: 'Native Client', filename: 'internal-nacl-plugin', description: '' },
70
+ ];
71
+ plugins.length = 3;
72
+ return plugins;
73
+ },
74
+ configurable: true,
75
+ });
76
+
77
+ // \u2500\u2500 navigator.languages \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
78
+ Object.defineProperty(navigator, 'languages', {
79
+ get: () => ['en-US', 'en'],
80
+ configurable: true,
81
+ });
82
+
83
+ // \u2500\u2500 navigator.permissions.query \u2014 hide "denied" for notifications \u2500
84
+ const originalQuery = window.navigator.permissions.query.bind(window.navigator.permissions);
85
+ window.navigator.permissions.query = (params) => {
86
+ if (params.name === 'notifications') {
87
+ return Promise.resolve({ state: 'prompt', onchange: null });
88
+ }
89
+ return originalQuery(params);
90
+ };
91
+
92
+ // \u2500\u2500 chrome runtime \u2014 make it look like a real Chrome \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
93
+ if (!window.chrome) {
94
+ window.chrome = {};
95
+ }
96
+ if (!window.chrome.runtime) {
97
+ window.chrome.runtime = {
98
+ connect: () => {},
99
+ sendMessage: () => {},
100
+ };
101
+ }
102
+
103
+ // \u2500\u2500 WebGL renderer \u2014 mask headless indicators \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
104
+ const getParameterOrig = WebGLRenderingContext.prototype.getParameter;
105
+ WebGLRenderingContext.prototype.getParameter = function(param) {
106
+ if (param === 37445) return 'Intel Inc.';
107
+ if (param === 37446) return 'Intel Iris OpenGL Engine';
108
+ return getParameterOrig.call(this, param);
109
+ };
110
+
111
+ // \u2500\u2500 WebGL2 renderer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
112
+ if (typeof WebGL2RenderingContext !== 'undefined') {
113
+ const getParam2Orig = WebGL2RenderingContext.prototype.getParameter;
114
+ WebGL2RenderingContext.prototype.getParameter = function(param) {
115
+ if (param === 37445) return 'Intel Inc.';
116
+ if (param === 37446) return 'Intel Iris OpenGL Engine';
117
+ return getParam2Orig.call(this, param);
118
+ };
119
+ }
120
+
121
+ // \u2500\u2500 Prevent iframe detection of automation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
122
+ Object.defineProperty(HTMLIFrameElement.prototype, 'contentWindow', {
123
+ get: function() {
124
+ return window;
125
+ },
126
+ });
127
+
128
+ // \u2500\u2500 Remove "cdc_" Playwright/ChromeDriver markers from DOM \u2500\u2500\u2500\u2500\u2500\u2500\u2500
129
+ const observer = new MutationObserver((mutations) => {
130
+ for (const mutation of mutations) {
131
+ for (const node of mutation.addedNodes) {
132
+ if (node.nodeType === 1) {
133
+ const el = node;
134
+ for (const attr of [...el.attributes]) {
135
+ if (attr.name.startsWith('cdc_') || attr.name.startsWith('__playwright')) {
136
+ el.removeAttribute(attr.name);
137
+ }
138
+ }
139
+ }
140
+ }
141
+ }
142
+ });
143
+ observer.observe(document.documentElement, { attributes: true, childList: true, subtree: true });
144
+ `;
145
+ }
146
+ function buildStealthContextOpts(config, viewport) {
147
+ const opts = {
148
+ viewport,
149
+ userAgent: config.userAgent ?? pickUserAgent(),
150
+ locale: config.locale ?? "en-US",
151
+ timezoneId: config.timezone ?? "America/New_York",
152
+ colorScheme: "light",
153
+ deviceScaleFactor: 2,
154
+ hasTouch: false,
155
+ javaScriptEnabled: true,
156
+ ignoreHTTPSErrors: config.ignoreHTTPSErrors ?? false
157
+ };
158
+ if (config.geolocation) {
159
+ opts.geolocation = config.geolocation;
160
+ opts.permissions = ["geolocation"];
161
+ }
162
+ return opts;
163
+ }
164
+ function buildStealthLaunchArgs(config) {
165
+ const args = [
166
+ "--disable-blink-features=AutomationControlled",
167
+ "--disable-infobars",
168
+ "--disable-dev-shm-usage",
169
+ "--no-first-run",
170
+ "--no-default-browser-check"
171
+ ];
172
+ const proxy = config.proxy ? { server: config.proxy.server, username: config.proxy.username, password: config.proxy.password } : void 0;
173
+ return { args, proxy };
174
+ }
175
+
176
+ // src/browser-provider.ts
177
+ var BrowserProvider = class {
178
+ browser = null;
179
+ context = null;
180
+ page = null;
181
+ pages = /* @__PURE__ */ new Map();
182
+ activeTabId = "tab-0";
183
+ tabCounter = 0;
184
+ _viewport;
185
+ _videoDir;
186
+ _humanize;
187
+ constructor() {
188
+ this._viewport = { width: 1280, height: 720 };
189
+ }
190
+ // ── Lifecycle ────────────────────────────────────────────────────────
191
+ async launch(opts) {
192
+ const pw = await import("playwright");
193
+ const chromium = pw.chromium;
194
+ this._viewport = opts?.viewport ?? { width: 1280, height: 720 };
195
+ const stealthEnabled = !!opts?.stealth;
196
+ const stealthCfg = typeof opts?.stealth === "object" ? opts.stealth : {};
197
+ if (opts?.humanize) {
198
+ const h = typeof opts.humanize === "object" ? opts.humanize : {};
199
+ this._humanize = {
200
+ typingDelay: h.typingDelay ?? [40, 120],
201
+ clickJitter: h.clickJitter ?? 3,
202
+ actionDelay: h.actionDelay ?? [200, 800],
203
+ mouseMovement: h.mouseMovement ?? true
204
+ };
205
+ }
206
+ const launchOpts = {
207
+ headless: opts?.headless ?? true
208
+ };
209
+ if (stealthEnabled) {
210
+ const { args, proxy } = buildStealthLaunchArgs(stealthCfg);
211
+ launchOpts.args = args;
212
+ if (proxy) launchOpts.proxy = proxy;
213
+ }
214
+ this.browser = await chromium.launch(launchOpts);
215
+ let contextOpts;
216
+ if (stealthEnabled) {
217
+ contextOpts = buildStealthContextOpts(stealthCfg, this._viewport);
218
+ } else {
219
+ contextOpts = {
220
+ viewport: this._viewport,
221
+ userAgent: pickUserAgent()
222
+ };
223
+ }
224
+ if (opts?.storageState) {
225
+ contextOpts.storageState = opts.storageState;
226
+ }
227
+ if (opts?.recordVideo) {
228
+ const dir = typeof opts.recordVideo === "object" ? opts.recordVideo.dir : "./browser-videos";
229
+ contextOpts.recordVideo = { dir, size: this._viewport };
230
+ this._videoDir = dir;
231
+ }
232
+ this.context = await this.browser.newContext(contextOpts);
233
+ if (stealthEnabled && stealthCfg.patchFingerprint !== false) {
234
+ await this.context.addInitScript(getStealthScript());
235
+ }
236
+ this.page = await this.context.newPage();
237
+ this.tabCounter = 0;
238
+ this.activeTabId = "tab-0";
239
+ this.pages.set("tab-0", this.page);
240
+ }
241
+ // ── Cookie / Auth Persistence ────────────────────────────────────────
242
+ async saveStorageState(path) {
243
+ this.ensureContext();
244
+ await this.context.storageState({ path });
245
+ }
246
+ // ── Navigation ───────────────────────────────────────────────────────
247
+ async navigate(url) {
248
+ this.ensurePage();
249
+ if (!/^https?:\/\//i.test(url)) {
250
+ throw new Error(`Invalid URL scheme: only http:// and https:// are allowed`);
251
+ }
252
+ await this.page.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 });
253
+ await this.waitForStable(500);
254
+ }
255
+ async back() {
256
+ this.ensurePage();
257
+ await this.page.goBack({ waitUntil: "domcontentloaded", timeout: 15e3 });
258
+ }
259
+ // ── Screenshot ───────────────────────────────────────────────────────
260
+ async screenshot() {
261
+ this.ensurePage();
262
+ return await this.page.screenshot({ type: "png", fullPage: false });
263
+ }
264
+ // ── Interaction (with optional humanize) ─────────────────────────────
265
+ async click(x, y) {
266
+ this.ensurePage();
267
+ const [fx, fy] = this.jitter(x, y);
268
+ if (this._humanize?.mouseMovement) {
269
+ await this.humanMouseMove(fx, fy);
270
+ }
271
+ await this.page.mouse.click(fx, fy);
272
+ await this.humanPause();
273
+ }
274
+ async type(text) {
275
+ this.ensurePage();
276
+ const delay = this._humanize ? this.randInt(this._humanize.typingDelay[0], this._humanize.typingDelay[1]) : 30;
277
+ await this.page.keyboard.type(text, { delay });
278
+ await this.humanPause();
279
+ }
280
+ async clickAndType(x, y, text) {
281
+ await this.click(x, y);
282
+ await this.sleep(this._humanize ? this.randInt(150, 350) : 200);
283
+ const [fx, fy] = this.jitter(x, y);
284
+ await this.page.mouse.click(fx, fy, { clickCount: 3 });
285
+ await this.sleep(this._humanize ? this.randInt(80, 200) : 100);
286
+ await this.type(text);
287
+ }
288
+ async pressKey(key) {
289
+ this.ensurePage();
290
+ await this.page.keyboard.press(key);
291
+ }
292
+ async scroll(direction, amount) {
293
+ this.ensurePage();
294
+ const base = amount ?? 400;
295
+ const jittered = this._humanize ? base + this.randInt(-40, 40) : base;
296
+ const scrollY = direction === "down" ? jittered : -jittered;
297
+ if (this._humanize) {
298
+ const steps = this.randInt(2, 4);
299
+ const perStep = scrollY / steps;
300
+ for (let i = 0; i < steps; i++) {
301
+ await this.page.mouse.wheel(0, perStep);
302
+ await this.sleep(this.randInt(30, 80));
303
+ }
304
+ } else {
305
+ await this.page.mouse.wheel(0, scrollY);
306
+ }
307
+ await this.humanPause();
308
+ }
309
+ // ── DOM Extraction ───────────────────────────────────────────────────
310
+ async extractDOM(opts) {
311
+ this.ensurePage();
312
+ const max = opts?.maxElements ?? 80;
313
+ const elements = await this.page.evaluate((limit) => {
314
+ const selectors = [
315
+ "a[href]",
316
+ "button",
317
+ "input",
318
+ "textarea",
319
+ "select",
320
+ "[role='button']",
321
+ "[role='link']",
322
+ "[role='tab']",
323
+ "[role='menuitem']",
324
+ "[onclick]",
325
+ "[contenteditable='true']"
326
+ ];
327
+ const all = globalThis.document.querySelectorAll(selectors.join(","));
328
+ const lines = [];
329
+ let count = 0;
330
+ const vh = globalThis.window.innerHeight;
331
+ const vw = globalThis.window.innerWidth;
332
+ for (const el of all) {
333
+ if (count >= limit) break;
334
+ const rect = el.getBoundingClientRect();
335
+ if (rect.width === 0 || rect.height === 0) continue;
336
+ if (rect.bottom < 0 || rect.top > vh) continue;
337
+ if (rect.right < 0 || rect.left > vw) continue;
338
+ const tag = el.tagName.toLowerCase();
339
+ const role = el.getAttribute("role") || tag;
340
+ const type = el.getAttribute("type") || "";
341
+ const text = (el.textContent || "").trim().slice(0, 80);
342
+ const placeholder = el.getAttribute("placeholder") || "";
343
+ const ariaLabel = el.getAttribute("aria-label") || "";
344
+ const href = el.getAttribute("href") || "";
345
+ const value = el.value || "";
346
+ const cx = Math.round(rect.left + rect.width / 2);
347
+ const cy = Math.round(rect.top + rect.height / 2);
348
+ let label = ariaLabel || text || placeholder || value;
349
+ if (!label && href) label = href.slice(0, 60);
350
+ if (!label) label = `(${tag}${type ? ` type=${type}` : ""})`;
351
+ lines.push(`[${cx},${cy}] ${role}${type ? `(${type})` : ""}: "${label}"`);
352
+ count++;
353
+ }
354
+ return lines.join("\n");
355
+ }, max);
356
+ return elements;
357
+ }
358
+ // ── Page Info ────────────────────────────────────────────────────────
359
+ async getPageInfo() {
360
+ this.ensurePage();
361
+ const url = this.page.url();
362
+ let title = "";
363
+ try {
364
+ title = await this.page.title();
365
+ } catch (err) {
366
+ console.warn("[agentium/browser] Error getting page title:", err instanceof Error ? err.message : err);
367
+ }
368
+ return { url, title, viewportSize: this._viewport };
369
+ }
370
+ async waitForStable(minWait = 300) {
371
+ this.ensurePage();
372
+ await this.sleep(minWait);
373
+ try {
374
+ await this.page.waitForLoadState("networkidle", { timeout: 5e3 });
375
+ } catch (err) {
376
+ console.warn("[agentium/browser] Error waiting for stable:", err instanceof Error ? err.message : err);
377
+ }
378
+ }
379
+ // ── Multi-Tab / Parallel Browsing ────────────────────────────────────
380
+ async newTab(url) {
381
+ this.ensureContext();
382
+ const newPage = await this.context.newPage();
383
+ this.tabCounter++;
384
+ const tabId = `tab-${this.tabCounter}`;
385
+ this.pages.set(tabId, newPage);
386
+ if (url) {
387
+ await newPage.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 });
388
+ }
389
+ return tabId;
390
+ }
391
+ async switchTab(tabId) {
392
+ const targetPage = this.pages.get(tabId);
393
+ if (!targetPage) throw new Error(`Tab "${tabId}" not found`);
394
+ this.page = targetPage;
395
+ this.activeTabId = tabId;
396
+ await this.page.bringToFront();
397
+ }
398
+ async closeTab(tabId) {
399
+ if (this.pages.size <= 1) throw new Error("Cannot close the last tab");
400
+ const targetPage = this.pages.get(tabId);
401
+ if (!targetPage) throw new Error(`Tab "${tabId}" not found`);
402
+ await targetPage.close();
403
+ this.pages.delete(tabId);
404
+ if (this.activeTabId === tabId) {
405
+ const firstRemaining = this.pages.entries().next().value;
406
+ if (firstRemaining) {
407
+ this.activeTabId = firstRemaining[0];
408
+ this.page = firstRemaining[1];
409
+ }
410
+ }
411
+ }
412
+ listTabs() {
413
+ const tabs = [];
414
+ for (const [id, pg] of this.pages) {
415
+ tabs.push({ id, url: pg.url(), active: id === this.activeTabId });
416
+ }
417
+ return tabs;
418
+ }
419
+ get currentTabId() {
420
+ return this.activeTabId;
421
+ }
422
+ // ── Video Recording ──────────────────────────────────────────────────
423
+ async getVideoPath(tabId) {
424
+ const targetPage = tabId ? this.pages.get(tabId) : this.page;
425
+ if (!targetPage) return null;
426
+ try {
427
+ const video = targetPage.video();
428
+ if (!video) return null;
429
+ return await video.path();
430
+ } catch (err) {
431
+ console.warn("[agentium/browser] Error getting video path:", err instanceof Error ? err.message : err);
432
+ return null;
433
+ }
434
+ }
435
+ get videoDir() {
436
+ return this._videoDir;
437
+ }
438
+ // ── Cleanup ──────────────────────────────────────────────────────────
439
+ async close() {
440
+ try {
441
+ if (this.context) await this.context.close();
442
+ } catch (err) {
443
+ console.warn("[agentium/browser] Error closing context:", err instanceof Error ? err.message : err);
444
+ }
445
+ try {
446
+ if (this.browser) await this.browser.close();
447
+ } catch (err) {
448
+ console.warn("[agentium/browser] Error closing browser:", err instanceof Error ? err.message : err);
449
+ }
450
+ this.page = null;
451
+ this.context = null;
452
+ this.browser = null;
453
+ this.pages.clear();
454
+ }
455
+ // ── Private: Humanize helpers ────────────────────────────────────────
456
+ /** Add small random offset to coordinates to avoid pixel-perfect bot patterns. */
457
+ jitter(x, y) {
458
+ if (!this._humanize) return [x, y];
459
+ const j = this._humanize.clickJitter;
460
+ return [x + this.randInt(-j, j), y + this.randInt(-j, j)];
461
+ }
462
+ /**
463
+ * Simulate human mouse movement using Bézier-like interpolation.
464
+ * Moves from the current mouse position to the target in small steps.
465
+ */
466
+ async humanMouseMove(targetX, targetY) {
467
+ const steps = this.randInt(5, 12);
468
+ const startX = this._viewport.width / 2;
469
+ const startY = this._viewport.height / 2;
470
+ for (let i = 1; i <= steps; i++) {
471
+ const t = i / steps;
472
+ const ease = t * t * (3 - 2 * t);
473
+ const cx = startX + (targetX - startX) * ease + this.randInt(-2, 2);
474
+ const cy = startY + (targetY - startY) * ease + this.randInt(-2, 2);
475
+ await this.page.mouse.move(cx, cy);
476
+ await this.sleep(this.randInt(5, 20));
477
+ }
478
+ await this.page.mouse.move(targetX, targetY);
479
+ }
480
+ /** Small random pause after an interaction. */
481
+ async humanPause() {
482
+ if (!this._humanize) return;
483
+ const [min, max] = this._humanize.actionDelay;
484
+ await this.sleep(this.randInt(min, max));
485
+ }
486
+ randInt(min, max) {
487
+ return Math.floor(Math.random() * (max - min + 1)) + min;
488
+ }
489
+ ensurePage() {
490
+ if (!this.page) throw new Error("Browser not launched. Call launch() first.");
491
+ }
492
+ ensureContext() {
493
+ if (!this.context) throw new Error("Browser not launched. Call launch() first.");
494
+ }
495
+ sleep(ms) {
496
+ return new Promise((resolve) => setTimeout(resolve, ms));
497
+ }
498
+ };
499
+
500
+ // src/prompts.ts
501
+ function buildSystemPrompt(viewport, extraInstructions, credentialKeys) {
502
+ const lines = [
503
+ `You are a browser automation agent. You receive a screenshot of a web browser and decide what action to take next to complete the user's task.`,
504
+ ``,
505
+ `## Viewport`,
506
+ `The browser viewport is ${viewport.width}\xD7${viewport.height} pixels. All coordinates you provide must be within this range.`,
507
+ ``,
508
+ `## Available Actions`,
509
+ `Respond with a JSON object containing one of these actions:`,
510
+ ``,
511
+ `### click`,
512
+ `Click at a specific coordinate. Use for buttons, links, inputs, checkboxes, etc.`,
513
+ `\`{ "action": "click", "x": <number>, "y": <number>, "description": "<what you are clicking>" }\``,
514
+ ``,
515
+ `### type`,
516
+ `Type text. If x/y are provided, click that position first (to focus the input), then type. If omitted, types into the currently focused element. To press Enter after typing (e.g., to submit a search), append "\\n" to the text.`,
517
+ `\`{ "action": "type", "text": "<text to type>", "x": <number|optional>, "y": <number|optional> }\``,
518
+ `Example: \`{ "action": "type", "text": "search query\\n", "x": 640, "y": 300 }\` \u2014 clicks the search box, types, and presses Enter.`,
519
+ ``,
520
+ `### scroll`,
521
+ `Scroll the page up or down. Use when content is below or above the visible area.`,
522
+ `\`{ "action": "scroll", "direction": "up"|"down", "amount": <pixels, optional, default 400> }\``,
523
+ ``,
524
+ `### navigate`,
525
+ `Navigate to a specific URL. Use when you know the exact URL to visit.`,
526
+ `\`{ "action": "navigate", "url": "<full URL>" }\``,
527
+ ``,
528
+ `### back`,
529
+ `Go back to the previous page.`,
530
+ `\`{ "action": "back" }\``,
531
+ ``,
532
+ `### wait`,
533
+ `Wait for the page to load or for a timed event. Use sparingly.`,
534
+ `\`{ "action": "wait", "ms": <milliseconds> }\``,
535
+ ``,
536
+ `### done`,
537
+ `The task is complete. Provide a summary of what was accomplished.`,
538
+ `\`{ "action": "done", "result": "<summary of what was accomplished>" }\``,
539
+ ``,
540
+ `### fail`,
541
+ `The task cannot be completed. Explain why.`,
542
+ `\`{ "action": "fail", "reason": "<why the task failed>" }\``,
543
+ ``,
544
+ `## Rules`,
545
+ `1. ALWAYS look at the screenshot carefully before deciding your action.`,
546
+ `2. Provide coordinates that target the CENTER of the element you want to interact with.`,
547
+ `3. For text inputs: click the input field first (using "type" with x/y), then the text will be typed.`,
548
+ `4. After typing in a search box, you often need to press Enter \u2014 use type with text "\\n" or click the search/submit button.`,
549
+ `5. If your previous action didn't produce the expected result, try a different approach.`,
550
+ `6. If a page is loading or blank, use "wait" with a short delay and try again.`,
551
+ `7. If you see a cookie banner or popup, dismiss it first before proceeding with the task.`,
552
+ `8. NEVER hallucinate content. Only report what you can actually see on the screen.`,
553
+ `9. When the task is fully complete, use "done" immediately with a comprehensive result.`,
554
+ `10. If after several attempts you cannot complete the task, use "fail" with a clear reason.`,
555
+ ``,
556
+ `## Response Format`,
557
+ `Respond with ONLY a valid JSON object. No markdown, no explanation, just the JSON action.`
558
+ ];
559
+ if (credentialKeys && credentialKeys.length > 0) {
560
+ lines.push(
561
+ ``,
562
+ `## Secure Credentials`,
563
+ `The following credential placeholders are available for use in "type" actions:`,
564
+ ...credentialKeys.map((k) => `- \`{{${k}}}\``),
565
+ ``,
566
+ `When you need to fill in a login form or any field requiring these credentials,`,
567
+ `use the EXACT placeholder (e.g. \`{{email}}\`) as the "text" value in a type action.`,
568
+ `The system will securely replace them with real values at execution time.`,
569
+ `NEVER guess, invent, or ask the user for the actual credential values.`,
570
+ `NEVER include real credential values in "done" or "fail" results.`
571
+ );
572
+ }
573
+ if (extraInstructions) {
574
+ lines.push(``, `## Additional Instructions`, extraInstructions);
575
+ }
576
+ return lines.join("\n");
577
+ }
578
+ function buildUserMessage(task, pageUrl, pageTitle, stepIndex, actionHistory, domSnapshot) {
579
+ const lines = [];
580
+ lines.push(`**Task:** ${task}`);
581
+ lines.push(`**Current URL:** ${pageUrl}`);
582
+ if (pageTitle) lines.push(`**Page Title:** ${pageTitle}`);
583
+ lines.push(`**Step:** ${stepIndex + 1}`);
584
+ if (domSnapshot) {
585
+ lines.push(``);
586
+ lines.push(`**Interactive elements on page (format: [centerX,centerY] role: "label"):**`);
587
+ lines.push(domSnapshot);
588
+ }
589
+ if (actionHistory.length > 0) {
590
+ lines.push(``);
591
+ lines.push(`**Previous actions:**`);
592
+ for (const entry of actionHistory.slice(-10)) {
593
+ lines.push(`- ${entry}`);
594
+ }
595
+ }
596
+ lines.push(``);
597
+ lines.push(`Look at the screenshot and decide the next action to complete the task.`);
598
+ return lines.join("\n");
599
+ }
600
+ function summarizeAction(action) {
601
+ switch (action.action) {
602
+ case "click":
603
+ return `Clicked at (${action.x}, ${action.y}): ${action.description}`;
604
+ case "type":
605
+ return action.x != null ? `Clicked (${action.x}, ${action.y}) and typed "${action.text}"` : `Typed "${action.text}"`;
606
+ case "scroll":
607
+ return `Scrolled ${action.direction}${action.amount ? ` ${action.amount}px` : ""}`;
608
+ case "navigate":
609
+ return `Navigated to ${action.url}`;
610
+ case "back":
611
+ return `Went back to previous page`;
612
+ case "wait":
613
+ return `Waited ${action.ms}ms`;
614
+ case "screenshot":
615
+ return `Took an extra screenshot`;
616
+ case "done":
617
+ return `Done: ${action.result}`;
618
+ case "fail":
619
+ return `Failed: ${action.reason}`;
620
+ default:
621
+ return JSON.stringify(action);
622
+ }
623
+ }
624
+
625
+ // src/browser-agent.ts
626
+ var BrowserAgent = class {
627
+ name;
628
+ eventBus;
629
+ model;
630
+ instructions;
631
+ maxSteps;
632
+ headless;
633
+ viewport;
634
+ defaultStartUrl;
635
+ waitAfterAction;
636
+ maxRepeats;
637
+ useDOM;
638
+ storageState;
639
+ recordVideo;
640
+ credentials;
641
+ stealth;
642
+ humanize;
643
+ costTracker;
644
+ memoryManager = null;
645
+ logger;
646
+ /** Access the MemoryManager (if memory is configured). */
647
+ get memory() {
648
+ return this.memoryManager;
649
+ }
650
+ constructor(config) {
651
+ this.name = config.name;
652
+ this.model = config.model;
653
+ this.instructions = config.instructions;
654
+ this.maxSteps = config.maxSteps ?? 30;
655
+ this.headless = config.headless ?? true;
656
+ this.viewport = config.viewport ?? { width: 1280, height: 720 };
657
+ this.defaultStartUrl = config.startUrl;
658
+ this.waitAfterAction = config.waitAfterAction ?? 1500;
659
+ this.maxRepeats = config.maxRepeats ?? 3;
660
+ this.useDOM = config.useDOM ?? false;
661
+ this.storageState = config.storageState;
662
+ this.recordVideo = config.recordVideo;
663
+ this.credentials = config.credentials;
664
+ this.stealth = config.stealth;
665
+ this.humanize = config.humanize;
666
+ this.costTracker = config.costTracker ?? null;
667
+ this.eventBus = config.eventBus ?? new import_core.EventBus();
668
+ this.logger = new import_core.Logger({
669
+ prefix: `BrowserAgent:${config.name}`,
670
+ level: config.logLevel ?? "silent"
671
+ });
672
+ if (config.memory) {
673
+ this.memoryManager = new import_core.MemoryManager(config.memory);
674
+ }
675
+ }
676
+ async run(task, opts) {
677
+ const startTime = Date.now();
678
+ const startUrl = opts?.startUrl ?? this.defaultStartUrl;
679
+ const sessionId = opts?.sessionId ?? `browser_${Date.now()}`;
680
+ const userId = opts?.userId;
681
+ const browser = new BrowserProvider();
682
+ const steps = [];
683
+ const actionHistory = [];
684
+ let extraInstructions = this.instructions ?? "";
685
+ if (this.memoryManager) {
686
+ await this.memoryManager.ensureReady();
687
+ const memoryContext = await this.memoryManager.buildContext(sessionId, userId, task, this.name);
688
+ if (memoryContext) {
689
+ extraInstructions = extraInstructions ? `${extraInstructions}
690
+
691
+ ${memoryContext}` : memoryContext;
692
+ }
693
+ }
694
+ const credentialKeys = this.credentials?.keys();
695
+ const systemPrompt = buildSystemPrompt(this.viewport, extraInstructions || void 0, credentialKeys);
696
+ let lastActionKey = "";
697
+ let repeatCount = 0;
698
+ try {
699
+ this.logger.info("Launching browser", {
700
+ headless: this.headless,
701
+ viewport: this.viewport,
702
+ useDOM: this.useDOM,
703
+ recordVideo: !!this.recordVideo,
704
+ stealth: !!this.stealth,
705
+ humanize: !!this.humanize
706
+ });
707
+ await browser.launch({
708
+ headless: this.headless,
709
+ viewport: this.viewport,
710
+ storageState: this.storageState,
711
+ recordVideo: this.recordVideo,
712
+ stealth: this.stealth,
713
+ humanize: this.humanize
714
+ });
715
+ if (startUrl) {
716
+ this.logger.info("Navigating to start URL", { url: startUrl });
717
+ await browser.navigate(startUrl);
718
+ }
719
+ for (let step = 0; step < this.maxSteps; step++) {
720
+ const screenshot = await browser.screenshot();
721
+ const pageInfo = await browser.getPageInfo();
722
+ let domSnapshot;
723
+ if (this.useDOM) {
724
+ domSnapshot = await browser.extractDOM();
725
+ }
726
+ this.eventBus.emit("browser.screenshot", { data: screenshot });
727
+ const userText = buildUserMessage(task, pageInfo.url, pageInfo.title, step, actionHistory, domSnapshot);
728
+ const messages = [
729
+ { role: "system", content: systemPrompt },
730
+ {
731
+ role: "user",
732
+ content: [
733
+ {
734
+ type: "image",
735
+ data: screenshot.toString("base64"),
736
+ mimeType: "image/png"
737
+ },
738
+ { type: "text", text: userText }
739
+ ]
740
+ }
741
+ ];
742
+ this.logger.debug("Sending screenshot to vision model", { step, url: pageInfo.url });
743
+ const response = await this.model.generate(messages, {
744
+ temperature: 0.1,
745
+ maxTokens: 1024,
746
+ apiKey: opts?.apiKey,
747
+ responseFormat: "json"
748
+ });
749
+ if (this.costTracker && response.usage) {
750
+ this.costTracker.track({
751
+ runId: sessionId,
752
+ agentName: this.name,
753
+ modelId: this.model.modelId,
754
+ usage: response.usage,
755
+ sessionId,
756
+ userId
757
+ });
758
+ }
759
+ const raw = typeof response.message.content === "string" ? response.message.content : "";
760
+ let action;
761
+ try {
762
+ action = JSON.parse(raw);
763
+ } catch {
764
+ this.logger.warn("Failed to parse model response as JSON, retrying", { raw });
765
+ actionHistory.push("(invalid JSON response \u2014 retrying)");
766
+ continue;
767
+ }
768
+ const step_ = {
769
+ index: step,
770
+ action,
771
+ screenshot,
772
+ pageUrl: pageInfo.url,
773
+ pageTitle: pageInfo.title,
774
+ timestamp: /* @__PURE__ */ new Date(),
775
+ dom: domSnapshot
776
+ };
777
+ steps.push(step_);
778
+ let summary = summarizeAction(action);
779
+ if (this.credentials) {
780
+ summary = this.credentials.mask(summary);
781
+ }
782
+ const actionKey = JSON.stringify(action);
783
+ if (actionKey === lastActionKey) {
784
+ repeatCount++;
785
+ } else {
786
+ lastActionKey = actionKey;
787
+ repeatCount = 1;
788
+ }
789
+ if (repeatCount > this.maxRepeats && action.action !== "done" && action.action !== "fail") {
790
+ this.logger.warn("Stuck in a loop \u2014 same action repeated", {
791
+ action: action.action,
792
+ repeats: repeatCount,
793
+ maxRepeats: this.maxRepeats
794
+ });
795
+ actionHistory.push(
796
+ `\u26A0 LOOP DETECTED: "${summary}" repeated ${repeatCount} times. The agent was stuck and auto-stopped. Try a different approach or a different startUrl.`
797
+ );
798
+ return await this.finalize(browser, steps, startTime, opts, {
799
+ result: `Stuck in a loop: "${summary}" was repeated ${repeatCount} times. The page may have a popup, consent banner, or unexpected state blocking progress.`,
800
+ success: false
801
+ });
802
+ }
803
+ actionHistory.push(summary);
804
+ this.logger.info(`Step ${step + 1}: ${summary}`);
805
+ this.eventBus.emit("browser.action", { action });
806
+ this.eventBus.emit("browser.step", {
807
+ index: step,
808
+ action,
809
+ pageUrl: pageInfo.url,
810
+ screenshot
811
+ });
812
+ if (action.action === "done") {
813
+ const result = this.credentials ? this.credentials.mask(action.result) : action.result;
814
+ return await this.finalize(browser, steps, startTime, opts, {
815
+ result,
816
+ success: true
817
+ });
818
+ }
819
+ if (action.action === "fail") {
820
+ const result = this.credentials ? this.credentials.mask(action.reason) : action.reason;
821
+ return await this.finalize(browser, steps, startTime, opts, {
822
+ result,
823
+ success: false
824
+ });
825
+ }
826
+ await this.executeAction(browser, action);
827
+ await this.sleep(this.waitAfterAction);
828
+ }
829
+ this.logger.warn("Max steps reached without completing task", { maxSteps: this.maxSteps });
830
+ return await this.finalize(browser, steps, startTime, opts, {
831
+ result: `Task not completed within ${this.maxSteps} steps. Last actions: ${actionHistory.slice(-3).join("; ")}`,
832
+ success: false
833
+ });
834
+ } catch (error) {
835
+ this.logger.error("Browser agent error", { error: error.message });
836
+ this.eventBus.emit("browser.error", { error });
837
+ await browser.close();
838
+ return {
839
+ result: `Error: ${error.message}`,
840
+ success: false,
841
+ steps,
842
+ finalUrl: "",
843
+ finalScreenshot: Buffer.alloc(0),
844
+ durationMs: Date.now() - startTime
845
+ };
846
+ }
847
+ }
848
+ /**
849
+ * Returns a ToolDef that lets a regular Agent delegate browser tasks
850
+ * to this BrowserAgent.
851
+ */
852
+ asTool(config) {
853
+ return {
854
+ name: config?.name ?? "browse_web",
855
+ description: config?.description ?? "Open a browser and autonomously complete a task on a website. Provide a clear task description and optionally a starting URL.",
856
+ parameters: import_zod.z.object({
857
+ task: import_zod.z.string().describe("What to do in the browser (e.g., 'Search for X and return the top 3 results')"),
858
+ startUrl: import_zod.z.string().optional().describe("URL to start at (e.g., 'https://www.google.com')")
859
+ }),
860
+ execute: async (args) => {
861
+ const result = await this.run(args.task, { startUrl: args.startUrl });
862
+ return result.result;
863
+ }
864
+ };
865
+ }
866
+ // ── Private helpers ──────────────────────────────────────────────────
867
+ async finalize(browser, steps, startTime, opts, outcome) {
868
+ const finalScreenshot = await browser.screenshot();
869
+ const finalInfo = await browser.getPageInfo();
870
+ if (opts?.saveStorageState) {
871
+ try {
872
+ await browser.saveStorageState(opts.saveStorageState);
873
+ this.logger.info("Storage state saved", { path: opts.saveStorageState });
874
+ } catch (e) {
875
+ this.logger.warn("Failed to save storage state", { error: e.message });
876
+ }
877
+ }
878
+ let videoPath;
879
+ if (this.recordVideo) {
880
+ videoPath = await browser.getVideoPath() ?? void 0;
881
+ }
882
+ await browser.close();
883
+ const output = {
884
+ result: outcome.result,
885
+ success: outcome.success,
886
+ steps,
887
+ finalUrl: finalInfo.url,
888
+ finalScreenshot,
889
+ durationMs: Date.now() - startTime,
890
+ videoPath
891
+ };
892
+ if (this.memoryManager) {
893
+ const sessionId = opts?.sessionId ?? `browser_${startTime}`;
894
+ const userId = opts?.userId;
895
+ const actionSummary = steps.map((s) => summarizeAction(s.action)).join("; ");
896
+ const messages = [
897
+ { role: "user", content: `Task: ${outcome.result}` },
898
+ { role: "assistant", content: `Actions: ${actionSummary}. Result: ${outcome.result}` }
899
+ ];
900
+ this.memoryManager.appendMessages(sessionId, messages, this.model).catch((e) => this.logger.warn("Memory persist failed", { error: String(e) }));
901
+ try {
902
+ this.memoryManager.afterRun(sessionId, userId, messages, this.model, this.name);
903
+ } catch (e) {
904
+ this.logger.warn("Memory afterRun failed", { error: String(e) });
905
+ }
906
+ }
907
+ this.eventBus.emit("browser.done", {
908
+ result: output.result,
909
+ success: outcome.success,
910
+ steps
911
+ });
912
+ return output;
913
+ }
914
+ async executeAction(browser, action) {
915
+ try {
916
+ switch (action.action) {
917
+ case "click":
918
+ await browser.click(action.x, action.y);
919
+ break;
920
+ case "type": {
921
+ const resolvedText = this.credentials ? this.credentials.resolve(action.text) : action.text;
922
+ if (action.x != null && action.y != null) {
923
+ await browser.clickAndType(action.x, action.y, resolvedText);
924
+ } else {
925
+ await browser.type(resolvedText);
926
+ }
927
+ if (resolvedText.includes("\n")) {
928
+ await browser.pressKey("Enter");
929
+ }
930
+ break;
931
+ }
932
+ case "scroll":
933
+ await browser.scroll(action.direction, action.amount);
934
+ break;
935
+ case "navigate":
936
+ await browser.navigate(action.url);
937
+ break;
938
+ case "back":
939
+ await browser.back();
940
+ break;
941
+ case "wait":
942
+ await this.sleep(Math.min(action.ms, 1e4));
943
+ break;
944
+ case "screenshot":
945
+ break;
946
+ default:
947
+ this.logger.warn("Unknown action", { action });
948
+ }
949
+ } catch (error) {
950
+ this.logger.warn("Action execution failed", { action: action.action, error: error.message });
951
+ }
952
+ }
953
+ sleep(ms) {
954
+ return new Promise((resolve) => setTimeout(resolve, ms));
955
+ }
956
+ };
957
+
958
+ // src/credential-vault.ts
959
+ var CredentialVault = class {
960
+ secrets = /* @__PURE__ */ new Map();
961
+ constructor(initial) {
962
+ if (initial) {
963
+ for (const [key, value] of Object.entries(initial)) {
964
+ this.set(key, value);
965
+ }
966
+ }
967
+ }
968
+ /** Store a credential. Key names become the placeholder: `{{key}}`. */
969
+ set(key, value) {
970
+ this.secrets.set(key.toLowerCase(), value);
971
+ return this;
972
+ }
973
+ /** Retrieve a credential value. Returns undefined if not found. */
974
+ get(key) {
975
+ return this.secrets.get(key.toLowerCase());
976
+ }
977
+ has(key) {
978
+ return this.secrets.has(key.toLowerCase());
979
+ }
980
+ /** List available placeholder names (never exposes values). */
981
+ keys() {
982
+ return [...this.secrets.keys()];
983
+ }
984
+ /**
985
+ * Load credentials from environment variables.
986
+ * Maps env var names to placeholder keys.
987
+ *
988
+ * @example
989
+ * vault.fromEnv({ email: "LOGIN_EMAIL", password: "LOGIN_PASS" });
990
+ */
991
+ fromEnv(mapping) {
992
+ for (const [key, envVar] of Object.entries(mapping)) {
993
+ const value = process.env[envVar];
994
+ if (value) this.set(key, value);
995
+ }
996
+ return this;
997
+ }
998
+ /**
999
+ * Replace `{{key}}` placeholders in text with actual credential values.
1000
+ * Used internally by BrowserAgent right before executing a type action.
1001
+ */
1002
+ resolve(text) {
1003
+ return text.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
1004
+ return this.get(key) ?? `{{${key}}}`;
1005
+ });
1006
+ }
1007
+ /**
1008
+ * Replace any occurrence of real credential values in text with
1009
+ * their `{{key}}` placeholder. Used to sanitize logs and action history.
1010
+ */
1011
+ mask(text) {
1012
+ let masked = text;
1013
+ for (const [key, value] of this.secrets) {
1014
+ if (value && masked.includes(value)) {
1015
+ masked = masked.split(value).join(`{{${key}}}`);
1016
+ }
1017
+ }
1018
+ return masked;
1019
+ }
1020
+ };
1021
+ // Annotate the CommonJS export names for ESM import in node:
1022
+ 0 && (module.exports = {
1023
+ BrowserAgent,
1024
+ BrowserProvider,
1025
+ CredentialVault
1026
+ });