<!-- Source: https://owlbrowser.net/node-sdk/ -->
# Node.js SDK

Node.js and TypeScript client for the Owl Browser HTTP server. ESM and async throughout, with one method per browser tool generated from the OpenAPI schema bundled in the package.

## Package Info
- Package: `@olib-ai/owl-browser-sdk`
- Requirements: Node.js 18+
- License: MIT

## Installation

Install via npm, pnpm, or yarn. Node 18 or newer. The package is ESM only.

```bash
npm install @olib-ai/owl-browser-sdk
```

## Quick Start

`createContext()` returns the context ID as a string. Every other tool takes it as the `context_id` parameter inside a single params object. Tool parameters keep the server's snake_case names; only client config fields are camelCase.

```typescript
import { OwlBrowser } from '@olib-ai/owl-browser-sdk';

const browser = new OwlBrowser({
  url: 'http://127.0.0.1:8080',
  token: 'your-secret-token',
  apiPrefix: ''  // '' for a direct connection, '/api' behind nginx
});

await browser.connect();

const contextId = await browser.createContext();

await browser.navigate({ context_id: contextId, url: 'https://example.com', wait_until: 'load' });
await browser.click({ context_id: contextId, selector: 'button#submit' });

const screenshot = await browser.screenshot({ context_id: contextId });
const markdown = await browser.getMarkdown({ context_id: contextId });
console.log(markdown);

await browser.closeContext({ context_id: contextId });
await browser.close();
```

## Calling Tools

Every tool in the OpenAPI schema bundled inside the package, not one fetched from the server, gets a generated method under both its camelCase and its snake_case name, with the `browser_` prefix removed. A handful of methods are hand written for better types and take precedence: `createContext`, `closeContext`, `closeAllContexts`, `navigate`, `click`, `type`, `screenshot`, `waitForSelector`, `getHtml`, `getMarkdown` and `evaluate`.

```typescript
// These three are exactly equivalent
await browser.getMarkdown({ context_id: contextId });
await browser.get_markdown({ context_id: contextId });
await browser.execute('browser_get_markdown', { context_id: contextId });

// execute() also takes an AbortSignal
await browser.execute('browser_navigate', { context_id: contextId, url: 'https://example.com' }, { signal });

// Introspection reads the bundled schema, so it works before connect()
browser.listTools();               // full tool names: 'browser_navigate', ...
browser.listMethods();             // generated names, camelCase and snake_case
browser.hasMethod('getMarkdown');  // true
browser.getTool('browser_navigate').requiredParams;  // ['context_id', 'url']
```

## Long-Running Tasks

An agentic run (`browser_nla`) routinely takes minutes and blows past the server's 30 second request timeout, dropping the connection while the browser keeps working. Submit it as a background job instead. `runTask()` is the one-call path: it submits, polls, and returns the unwrapped answer.

```typescript
// browser_nla needs an LLM on the context: the built-in model, or your own
const contextId = await browser.createContext({
  render_mode: 'agent',
  llm_use_builtin: false,
  llm_is_third_party: true,
  llm_endpoint: 'https://api.openai.com/v1',
  llm_model: 'gpt-4o',
  llm_api_key: process.env.OPENAI_API_KEY
});
await browser.navigate({ context_id: contextId, url: 'https://books.toscrape.com', wait_until: 'load' });

// timeoutMs and pollIntervalMs are milliseconds; any other key passes through to the tool
const answer = await browser.runTask({
  contextId,
  command: "Open the product page for 'The Great Railway Bazaar' and report how many copies are in stock.",
  timeoutMs: 600000,
  pollIntervalMs: 2000
});

console.log(answer);  // the tool's own result, envelopes already stripped
```

### The Job API

```typescript
// Use these when you need the job ID itself
const jobId = await browser.submitJob('browser_nla', {
  context_id: contextId,
  command: '...'
});

const job = await browser.getJob(jobId);   // one job record
const jobs = await browser.listJobs();     // live jobs, no result payloads
await browser.cancelJob(jobId);            // request cancellation

const answer = await browser.waitForJob(jobId, {
  timeoutMs: 600000,
  pollIntervalMs: 2000
});
```

### Job States

- `queued`: accepted, not started
- `running`: executing
- `done`: finished, result populated
- `failed`: finished, error populated
- `cancelled`: stopped before completion
- `cancelling`: cancel requested on a job already inside a browser call

A cancel cannot interrupt an in-flight browser call. A queued job moves straight to `cancelled`; a running one reports `cancelling` until that call returns. Keep polling through it, it is not terminal.

The job endpoints are plain REST and exist on the HTTP transport only. A client built with `TransportMode.WEBSOCKET` rejects with `OwlBrowserError`. Finished jobs are kept for 30 minutes.

## Authentication

### Bearer Token
The default. The transport throws if the token is missing and JWT auth is not configured.

```typescript
const browser = new OwlBrowser({
  url: 'http://127.0.0.1:8080',
  token: 'your-secret-token'
});
```

### JWT Authentication
RS256, re-signed automatically as it approaches expiry over HTTP.

```typescript
import { OwlBrowser, AuthMode } from '@olib-ai/owl-browser-sdk';

const browser = new OwlBrowser({
  url: 'http://127.0.0.1:8080',
  authMode: AuthMode.JWT,
  jwt: {
    privateKeyPath: '/path/to/private.pem',  // path or inline PEM
    expiresIn: 3600,
    refreshThreshold: 300,
    issuer: 'my-app',
    subject: 'automation'
  }
});
```

## Configuration

The constructor takes one `RemoteConfig` object. Paths are built as `{url}{apiPrefix}/execute/{tool}`, so keep the default `/api` behind nginx and pass an empty string when you talk to the browser server directly.

```typescript
interface RemoteConfig {
  url: string;                    // required, trailing slashes stripped

  token?: string;                 // required for token auth
  authMode?: AuthMode;            // AuthMode.TOKEN (default) or AuthMode.JWT
  jwt?: JWTConfig;                // required when authMode is JWT

  transport?: TransportMode;      // HTTP (default) or WEBSOCKET
  timeout?: number;               // SECONDS, default 30
  maxConcurrent?: number;         // default 10, HTTP only
  retry?: RetryConfig;            // HTTP only
  verifySsl?: boolean;            // accepted for parity with Python, currently inert
  apiPrefix?: string;             // default '/api', '' for a direct connection
}
```

`RetryConfig` defaults: `maxRetries: 3` (total attempts, not extra attempts), `initialDelayMs: 100`, `maxDelayMs: 10000`, `backoffMultiplier: 2.0`, `jitterFactor: 0.1`. Retries cover connection level failures only; timeouts, auth failures, rate limits and IP blocks are raised immediately. On HTTP, long running tools get `max(120s, timeout * 4)` instead of `timeout`.

## Error Handling

All errors extend `OwlBrowserError`. A failing tool surfaces as `ToolExecutionError`: a 4xx or 5xx other than 401, 403 and 429, or a JSON body with `success: false`.

```typescript
import {
  OwlBrowserError,
  ToolExecutionError,
  TimeoutError,
  RateLimitError,
  AuthenticationError
} from '@olib-ai/owl-browser-sdk';

try {
  await browser.click({ context_id: contextId, selector: '#nonexistent' });
} catch (e) {
  if (e instanceof ToolExecutionError) {
    console.log(e.toolName, e.status, e.message);
  } else if (e instanceof TimeoutError) {
    console.log('timed out after', e.timeoutMs, 'ms');
  } else if (e instanceof RateLimitError) {
    console.log('retry after', e.retryAfter, 'seconds');
  } else if (e instanceof AuthenticationError) {
    console.log(e.message);
  }
}
```

`ElementNotFoundError`, `NavigationError`, `ContextLimitError` and `ExpectationError` are exported but the transports never raise them. To get one, run a result through the exported `raiseForActionResult(result)` helper yourself.

## Flow Execution

An optional layer on top of the tool client. Runs declarative JSON flows: steps, captured variables, for_each, retries and expectations. Flows are portable between the Node and Python SDKs.

### Running a Flow

```typescript
import { OwlBrowser, FlowExecutor } from '@olib-ai/owl-browser-sdk';

const browser = new OwlBrowser({ url: '...', token: '...' });
await browser.connect();

const contextId = await browser.createContext();
const executor = new FlowExecutor(browser, contextId);

const flow = FlowExecutor.loadFlow('test-flows/navigation.json');
const result = await executor.execute(flow);

if (result.success) {
  console.log('Flow completed in', result.totalDurationMs, 'ms');
} else {
  console.error('Flow failed:', result.error);
}

await browser.closeContext({ context_id: contextId });
```

### Flow JSON Format

```json
{
  "name": "Login Flow",
  "steps": [
    {
      "type": "browser_navigate",
      "url": "https://example.com/login"
    },
    {
      "type": "browser_type",
      "selector": "#email",
      "text": "user@example.com"
    },
    {
      "type": "browser_click",
      "selector": "#submit",
      "expected": {
        "notEmpty": true
      }
    }
  ]
}
```
