@monostate/browsernative-client 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +44 -0
  2. package/index.d.ts +113 -0
  3. package/index.js +168 -0
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -112,6 +112,50 @@ const client = new BrowserNativeClient(apiKey, {
112
112
  }
113
113
  ```
114
114
 
115
+ ### Browser sessions
116
+
117
+ Persistent browser sessions with real-time control over WebSocket:
118
+
119
+ ```javascript
120
+ const session = await client.createSession({ mode: 'auto' });
121
+
122
+ await session.goto('https://example.com');
123
+ await session.click('#login');
124
+ await session.type('#email', 'user@example.com');
125
+ const state = await session.getPageState({ includeScreenshot: true });
126
+ await session.screenshot();
127
+
128
+ // Listen for backend fallback events
129
+ session.on('fallback', (e) => console.log(`${e.from} -> ${e.to}: ${e.reason}`));
130
+
131
+ session.close();
132
+ ```
133
+
134
+ Modes: `auto` (LightPanda + Chrome fallback), `headless`, `visual`, `computer-use`.
135
+
136
+ #### Computer use mode
137
+
138
+ For AI agents that control the browser by pixel coordinates:
139
+
140
+ ```javascript
141
+ const session = await client.createSession({ mode: 'computer-use', screenWidth: 1280, screenHeight: 800 });
142
+
143
+ await session.goto('https://example.com');
144
+ await session.clickAt(640, 400);
145
+ await session.typeText('hello world');
146
+ await session.mouseMove(100, 200);
147
+ await session.drag(10, 20, 300, 400);
148
+ await session.scrollAt(640, 400, 'down', 5);
149
+ const pos = await session.getCursorPosition();
150
+ const size = await session.getScreenSize();
151
+ const screenshot = await session.screenshot();
152
+
153
+ // VNC streaming URL
154
+ console.log(session.vncUrl);
155
+
156
+ session.close();
157
+ ```
158
+
115
159
  ## TypeScript
116
160
 
117
161
  Full type definitions included.
package/index.d.ts CHANGED
@@ -168,6 +168,114 @@ export interface HealthResult {
168
168
  responseTime: number;
169
169
  }
170
170
 
171
+ export interface SessionOptions {
172
+ /** Browser mode (default: 'auto') */
173
+ mode?: 'auto' | 'headless' | 'visual' | 'computer-use';
174
+ /** Screen width for computer-use mode */
175
+ screenWidth?: number;
176
+ /** Screen height for computer-use mode */
177
+ screenHeight?: number;
178
+ }
179
+
180
+ export interface PageState {
181
+ url: string;
182
+ title: string;
183
+ content?: string;
184
+ screenshot?: string;
185
+ interactiveElements?: Array<{
186
+ tag: string;
187
+ text?: string;
188
+ selector: string;
189
+ type?: string;
190
+ href?: string;
191
+ }>;
192
+ }
193
+
194
+ export interface FallbackEvent {
195
+ type: 'fallback';
196
+ from: string;
197
+ to: string;
198
+ reason: string;
199
+ }
200
+
201
+ export interface ClosedEvent {
202
+ type: 'closed';
203
+ reason: string;
204
+ }
205
+
206
+ export interface Cookie {
207
+ name: string;
208
+ value: string;
209
+ domain?: string;
210
+ path?: string;
211
+ expires?: number;
212
+ httpOnly?: boolean;
213
+ secure?: boolean;
214
+ sameSite?: 'Strict' | 'Lax' | 'None';
215
+ }
216
+
217
+ /**
218
+ * Persistent browser session over WebSocket
219
+ */
220
+ export declare class BrowserSession {
221
+ /** Session ID assigned by the server */
222
+ sessionId: string | null;
223
+ /** Current browser backend ('lightpanda', 'chrome', or 'computer-use') */
224
+ backend: string | null;
225
+ /** VNC URL for computer-use mode (null otherwise) */
226
+ vncUrl: string | null;
227
+
228
+ /** Navigate to a URL */
229
+ goto(url: string): Promise<any>;
230
+ /** Click an element */
231
+ click(selector: string, options?: { timeout?: number }): Promise<any>;
232
+ /** Type text into an element */
233
+ type(selector: string, text: string, options?: { clear?: boolean; delay?: number }): Promise<any>;
234
+ /** Scroll the page */
235
+ scroll(direction?: 'up' | 'down', amount?: number): Promise<any>;
236
+ /** Hover over an element */
237
+ hover(selector: string): Promise<any>;
238
+ /** Select option(s) in a dropdown */
239
+ select(selector: string, ...values: string[]): Promise<any>;
240
+ /** Press a keyboard key */
241
+ pressKey(key: string): Promise<any>;
242
+ /** Navigate back */
243
+ goBack(): Promise<any>;
244
+ /** Navigate forward */
245
+ goForward(): Promise<any>;
246
+ /** Take a screenshot */
247
+ screenshot(options?: { fullPage?: boolean; type?: 'png' | 'jpeg' | 'webp'; quality?: number }): Promise<any>;
248
+ /** Get current page state with interactive elements */
249
+ getPageState(options?: { includeScreenshot?: boolean }): Promise<PageState>;
250
+ /** Extract page content as structured data */
251
+ extractContent(): Promise<any>;
252
+ /** Wait for a selector to appear */
253
+ waitFor(selector: string, timeout?: number): Promise<any>;
254
+ /** Evaluate JavaScript in the page context */
255
+ evaluate(fn: string): Promise<any>;
256
+ /** Get all cookies */
257
+ getCookies(): Promise<Cookie[]>;
258
+ /** Set cookies */
259
+ setCookies(cookies: Cookie[]): Promise<any>;
260
+
261
+ /** Coordinate-based actions (computer-use mode) */
262
+ mouseMove(x: number, y: number): Promise<any>;
263
+ clickAt(x: number, y: number, button?: 'left' | 'right' | 'middle'): Promise<any>;
264
+ doubleClickAt(x: number, y: number, button?: 'left' | 'right' | 'middle'): Promise<any>;
265
+ drag(startX: number, startY: number, endX: number, endY: number): Promise<any>;
266
+ scrollAt(x: number, y: number, direction: 'up' | 'down', amount?: number): Promise<any>;
267
+ typeText(text: string): Promise<any>;
268
+ getCursorPosition(): Promise<{ x: number; y: number }>;
269
+ getScreenSize(): Promise<{ width: number; height: number }>;
270
+
271
+ /** Listen for session events */
272
+ on(event: 'fallback', handler: (event: FallbackEvent) => void): this;
273
+ on(event: 'closed', handler: (event: ClosedEvent) => void): this;
274
+
275
+ /** Close the session */
276
+ close(): void;
277
+ }
278
+
171
279
  /**
172
280
  * Browser Native API Client
173
281
  */
@@ -204,6 +312,11 @@ export declare class BrowserNativeClient {
204
312
  */
205
313
  getUsage(days?: number): Promise<UsageResult>;
206
314
 
315
+ /**
316
+ * Create a persistent browser session via WebSocket
317
+ */
318
+ createSession(options?: SessionOptions): Promise<BrowserSession>;
319
+
207
320
  /**
208
321
  * Check API health and your account status
209
322
  */
package/index.js CHANGED
@@ -143,6 +143,30 @@ export class BrowserNativeClient {
143
143
  return this._makeRequest('/stats', {}, 'GET');
144
144
  }
145
145
 
146
+ /**
147
+ * Create a persistent browser session via WebSocket
148
+ * @param {object} options - Session options
149
+ * @param {'auto'|'headless'|'visual'|'computer-use'} options.mode - Browser mode (default: 'auto')
150
+ * @param {number} [options.screenWidth] - Screen width for computer-use mode
151
+ * @param {number} [options.screenHeight] - Screen height for computer-use mode
152
+ * @returns {Promise<BrowserSession>} Connected browser session
153
+ */
154
+ async createSession(options = {}) {
155
+ const wsUrl = this.baseUrl.replace(/^http/, 'ws');
156
+ const mode = options.mode || 'auto';
157
+ const params = new URLSearchParams({
158
+ apiKey: this.apiKey,
159
+ mode,
160
+ });
161
+ if (options.screenWidth) params.set('screenWidth', String(options.screenWidth));
162
+ if (options.screenHeight) params.set('screenHeight', String(options.screenHeight));
163
+ const url = `${wsUrl}/session?${params}`;
164
+
165
+ const session = new BrowserSession(url, { verbose: this.verbose });
166
+ await session.connect();
167
+ return session;
168
+ }
169
+
146
170
  /**
147
171
  * Check API health and your account status
148
172
  * @returns {Promise<object>} Health check result
@@ -294,5 +318,149 @@ export async function bulkScrape(urls, apiKey, options = {}) {
294
318
  return client.bulkScrape(urls, options);
295
319
  }
296
320
 
321
+ // ── Browser Session (WebSocket) ──────────────────────────────────────────────
322
+
323
+ class BrowserSession {
324
+ constructor(url, options = {}) {
325
+ this._url = url;
326
+ this._ws = null;
327
+ this._nextId = 1;
328
+ this._pending = new Map(); // id → { resolve, reject }
329
+ this._verbose = options.verbose || false;
330
+ this._eventHandlers = {};
331
+ this.sessionId = null;
332
+ this.backend = null;
333
+ this.vncUrl = null;
334
+ }
335
+
336
+ async connect() {
337
+ return new Promise((resolve, reject) => {
338
+ const WebSocketImpl = typeof WebSocket !== 'undefined'
339
+ ? WebSocket
340
+ : null;
341
+
342
+ if (!WebSocketImpl) {
343
+ // Node.js — try dynamic import
344
+ import('ws').then(ws => {
345
+ this._ws = new ws.default(this._url);
346
+ this._wireEvents(resolve, reject);
347
+ }).catch(() => {
348
+ reject(new Error('WebSocket not available. Install "ws" package for Node.js: npm install ws'));
349
+ });
350
+ return;
351
+ }
352
+
353
+ this._ws = new WebSocketImpl(this._url);
354
+ this._wireEvents(resolve, reject);
355
+ });
356
+ }
357
+
358
+ _wireEvents(onConnected, onError) {
359
+ this._ws.onmessage = (event) => {
360
+ const data = typeof event.data === 'string' ? event.data : event.data.toString();
361
+ let msg;
362
+ try { msg = JSON.parse(data); } catch { return; }
363
+
364
+ if (msg.type === 'connected') {
365
+ this.sessionId = msg.sessionId;
366
+ this.backend = msg.backend;
367
+ this.vncUrl = msg.vncUrl || null;
368
+ if (this._verbose) console.log(`Browser Native: Session ${msg.sessionId} connected (${msg.backend})`);
369
+ onConnected(this);
370
+ return;
371
+ }
372
+
373
+ if (msg.type === 'fallback') {
374
+ this.backend = msg.to;
375
+ if (this._verbose) console.log(`Browser Native: Fallback ${msg.from} → ${msg.to} (${msg.reason})`);
376
+ if (this._eventHandlers.fallback) this._eventHandlers.fallback(msg);
377
+ return;
378
+ }
379
+
380
+ if (msg.type === 'closed') {
381
+ if (this._verbose) console.log(`Browser Native: Session closed (${msg.reason})`);
382
+ if (this._eventHandlers.closed) this._eventHandlers.closed(msg);
383
+ return;
384
+ }
385
+
386
+ // Match response to pending request by id
387
+ if (msg.id != null && this._pending.has(msg.id)) {
388
+ const { resolve, reject } = this._pending.get(msg.id);
389
+ this._pending.delete(msg.id);
390
+ if (msg.type === 'error') {
391
+ reject(new Error(msg.message));
392
+ } else {
393
+ if (msg.backend) this.backend = msg.backend;
394
+ resolve(msg.data);
395
+ }
396
+ }
397
+ };
398
+
399
+ this._ws.onerror = (err) => {
400
+ onError(new Error(err.message || 'WebSocket connection failed'));
401
+ };
402
+
403
+ this._ws.onclose = (event) => {
404
+ // Reject all pending requests
405
+ for (const [, { reject }] of this._pending) {
406
+ reject(new Error(`Session closed: ${event.reason || 'unknown'}`));
407
+ }
408
+ this._pending.clear();
409
+ };
410
+ }
411
+
412
+ _send(action, params = {}) {
413
+ return new Promise((resolve, reject) => {
414
+ if (!this._ws || this._ws.readyState !== 1) {
415
+ return reject(new Error('Session not connected'));
416
+ }
417
+ const id = this._nextId++;
418
+ this._pending.set(id, { resolve, reject });
419
+ this._ws.send(JSON.stringify({ id, action, ...params }));
420
+ });
421
+ }
422
+
423
+ on(event, handler) {
424
+ this._eventHandlers[event] = handler;
425
+ return this;
426
+ }
427
+
428
+ async goto(url) { return this._send('goto', { url }); }
429
+ async click(selector, options = {}) { return this._send('click', { selector, ...options }); }
430
+ async type(selector, text, options = {}) { return this._send('type', { selector, text, ...options }); }
431
+ async scroll(direction = 'down', amount = 500) { return this._send('scroll', { direction, amount }); }
432
+ async hover(selector) { return this._send('hover', { selector }); }
433
+ async select(selector, ...values) { return this._send('select', { selector, values }); }
434
+ async pressKey(key) { return this._send('pressKey', { key }); }
435
+ async goBack() { return this._send('goBack'); }
436
+ async goForward() { return this._send('goForward'); }
437
+ async screenshot(options = {}) { return this._send('screenshot', options); }
438
+ async getPageState(options = {}) { return this._send('getPageState', options); }
439
+ async extractContent() { return this._send('extractContent'); }
440
+ async waitFor(selector, timeout) { return this._send('waitFor', { selector, timeout }); }
441
+ async evaluate(fn) { return this._send('evaluate', { fn }); }
442
+ async getCookies() { return this._send('getCookies'); }
443
+ async setCookies(cookies) { return this._send('setCookies', { cookies }); }
444
+
445
+ // Coordinate-based actions (computer-use mode)
446
+ async mouseMove(x, y) { return this._send('mouseMove', { x, y }); }
447
+ async clickAt(x, y, button = 'left') { return this._send('clickAt', { x, y, button }); }
448
+ async doubleClickAt(x, y, button = 'left') { return this._send('doubleClickAt', { x, y, button }); }
449
+ async drag(startX, startY, endX, endY) { return this._send('drag', { startX, startY, endX, endY }); }
450
+ async scrollAt(x, y, direction, amount) { return this._send('scrollAt', { x, y, direction, amount }); }
451
+ async typeText(text) { return this._send('typeText', { text }); }
452
+ async getCursorPosition() { return this._send('getCursorPosition'); }
453
+ async getScreenSize() { return this._send('getScreenSize'); }
454
+
455
+ close() {
456
+ if (this._ws) {
457
+ this._ws.close();
458
+ this._ws = null;
459
+ }
460
+ }
461
+ }
462
+
463
+ export { BrowserSession };
464
+
297
465
  // Default export for CommonJS compatibility
298
466
  export default BrowserNativeClient;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monostate/browsernative-client",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "Browser Native client SDK for web scraping and content extraction API",
5
5
  "type": "module",
6
6
  "main": "index.js",