diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 5fdd883..c4ddc74 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.1.0" + ".": "1.1.1" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bf3d39..9c9675a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 1.1.1 (2025-08-20) + +Full Changelog: [v1.1.0...v1.1.1](https://github.com/browser-use/browser-use-node/compare/v1.1.0...v1.1.1) + ## 1.1.0 (2025-08-20) Full Changelog: [v1.0.0...v1.1.0](https://github.com/browser-use/browser-use-node/compare/v1.0.0...v1.1.0) diff --git a/README.md b/README.md index 5ca5e8c..22557fd 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,28 @@ -# Browser Use TypeScript API Library - -[![NPM version]()](https://npmjs.org/package/browser-use-sdk) ![npm bundle size](https://img.shields.io/bundlephobia/minzip/browser-use-sdk) - -This library provides convenient access to the Browser Use REST API from server-side TypeScript or JavaScript. - -The REST API documentation can be found on [docs.browser-use.com](https://docs.browser-use.com/cloud/). The full API of this library can be found in [api.md](api.md). - -It is generated with [Stainless](https://www.stainless.com/). - -## Installation +Browser Use JS ```sh -npm install browser-use-sdk +pnpm add browser-use-sdk ``` -## Usage +## QuickStart -The full API of this library can be found in [api.md](api.md). - - -```js +```ts import BrowserUse from 'browser-use-sdk'; -const client = new BrowserUse({ - apiKey: process.env['BROWSER_USE_API_KEY'], // This is the default and can be omitted -}); - -// #1 - Run a task and get its result +const client = new BrowserUse(); -const task = await client.tasks.run({ +const result = await client.tasks.run({ task: 'Search for the top 10 Hacker News posts and return the title and url.', }); -console.log(task.doneOutput); +console.log(result.doneOutput); +``` -// #2 - Run a task with a structured result +> The full API of this library can be found in [api.md](api.md). +### Structured Output with Zod + +```ts import z from 'zod'; const TaskOutput = z.object({ @@ -47,23 +34,25 @@ const TaskOutput = z.object({ ), }); -const posts = await client.tasks.run({ +const result = await client.tasks.run({ task: 'Search for the top 10 Hacker News posts and return the title and url.', }); -for (const post of posts.doneOutput.posts) { +for (const post of result.parsedOutput.posts) { console.log(`${post.title} - ${post.url}`); } +``` -// #3 - Stream Task Progress +### Streaming Agent Updates -const hn = await browseruse.tasks.create({ +```ts +const task = await browseruse.tasks.create({ task: 'Search for the top 10 Hacker News posts and return the title and url.', schema: TaskOutput, }); const stream = browseruse.tasks.stream({ - taskId: hn.id, + taskId: task.id, schema: TaskOutput, }); @@ -78,7 +67,7 @@ for await (const msg of stream) { case 'finished': console.log(`done:`); - for (const post of msg.doneOutput.posts) { + for (const post of msg.parsedOutput.posts) { console.log(`${post.title} - ${post.url}`); } break; diff --git a/assets/cloud-banner-js.png b/assets/cloud-banner-js.png new file mode 100644 index 0000000..fb9f50c Binary files /dev/null and b/assets/cloud-banner-js.png differ diff --git a/examples/demo.ts b/examples/demo.ts deleted file mode 100755 index 4579b78..0000000 --- a/examples/demo.ts +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env -S npm run tsn -T - -import { BrowserUse } from 'browser-use-sdk'; - -import { env, spinner } from './utils'; - -env(); - -// gets API Key from environment variable BROWSER_USE_API_KEY -const browseruse = new BrowserUse(); - -async function main() { - let log = 'starting'; - const stop = spinner(() => log); - - // Create Task - const rsp = await browseruse.tasks.create({ - task: "What's the weather line in SF and what's the temperature?", - }); - - poll: do { - // Wait for Task to Finish - const status = await browseruse.tasks.retrieve(rsp.id); - - switch (status.status) { - case 'started': - case 'paused': - case 'stopped': - log = `agent ${status.status} - live: ${status.session.liveUrl}`; - - await new Promise((resolve) => setTimeout(resolve, 2000)); - break; - - case 'finished': - stop(); - - console.log(status.doneOutput); - break poll; - } - } while (true); -} - -main().catch(console.error); diff --git a/examples/zod.ts b/examples/retrieve.ts similarity index 52% rename from examples/zod.ts rename to examples/retrieve.ts index dec48c3..613ab94 100755 --- a/examples/zod.ts +++ b/examples/retrieve.ts @@ -1,15 +1,47 @@ #!/usr/bin/env -S npm run tsn -T import { BrowserUse } from 'browser-use-sdk'; -import { z } from 'zod'; import { env, spinner } from './utils'; +import z from 'zod'; env(); // gets API Key from environment variable BROWSER_USE_API_KEY const browseruse = new BrowserUse(); +async function basic() { + let log = 'starting'; + const stop = spinner(() => log); + + // Create Task + const rsp = await browseruse.tasks.create({ + task: "What's the weather line in SF and what's the temperature?", + agentSettings: { llm: 'gemini-2.5-flash' }, + }); + + poll: do { + // Wait for Task to Finish + const status = await browseruse.tasks.retrieve(rsp.id); + + switch (status.status) { + case 'started': + case 'paused': + case 'stopped': + log = `agent ${status.status} - live: ${status.session.liveUrl}`; + + await new Promise((resolve) => setTimeout(resolve, 2000)); + break; + + case 'finished': + stop(); + + console.log(status.doneOutput); + break poll; + } + } while (true); +} + // Define Structured Output Schema const HackerNewsResponse = z.object({ title: z.string(), @@ -21,7 +53,7 @@ const TaskOutput = z.object({ posts: z.array(HackerNewsResponse), }); -async function main() { +async function structured() { let log = 'starting'; const stop = spinner(() => log); @@ -29,6 +61,7 @@ async function main() { const rsp = await browseruse.tasks.create({ task: 'Extract top 10 Hacker News posts and return the title, url, and score', schema: TaskOutput, + agentSettings: { llm: 'gpt-4.1' }, }); poll: do { @@ -42,13 +75,7 @@ async function main() { case 'started': case 'paused': case 'stopped': { - const stepsCount = status.steps ? status.steps.length : 0; - const steps = `${stepsCount} steps`; - const lastGoalDescription = stepsCount > 0 ? status.steps![stepsCount - 1]!.nextGoal : undefined; - const lastGoal = lastGoalDescription ? `, last: ${lastGoalDescription}` : ''; - const liveUrl = status.session.liveUrl ? `, live: ${status.session.liveUrl}` : ''; - - log = `agent ${status.status} (${steps}${lastGoal}${liveUrl}) `; + log = `agent ${status.status} ${status.session.liveUrl} | ${status.steps.length} steps`; await new Promise((resolve) => setTimeout(resolve, 2000)); @@ -56,16 +83,16 @@ async function main() { } case 'finished': - if (status.doneOutput == null) { + if (status.parsedOutput == null) { throw new Error('No output'); } stop(); // Print Structured Output - console.log('TOP POSTS:'); + console.log('Top Hacker News Posts:'); - for (const post of status.doneOutput.posts) { + for (const post of status.parsedOutput.posts) { console.log(` - ${post.title} (${post.score}) ${post.url}`); } @@ -74,4 +101,6 @@ async function main() { } while (true); } -main().catch(console.error); +basic() + .then(() => structured()) + .catch(console.error); diff --git a/examples/run-zod.ts b/examples/run-zod.ts deleted file mode 100755 index 54c316e..0000000 --- a/examples/run-zod.ts +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env -S npm run tsn -T - -import { BrowserUse } from 'browser-use-sdk'; - -import z from 'zod'; -import { env } from './utils'; - -env(); - -// gets API Key from environment variable BROWSER_USE_API_KEY -const browseruse = new BrowserUse(); - -const HackerNewsResponse = z.object({ - title: z.string(), - url: z.string(), - score: z.number(), -}); - -const TaskOutput = z.object({ - posts: z.array(HackerNewsResponse), -}); - -async function main() { - console.log(`Running Task...`); - - // Create Task - const rsp = await browseruse.tasks.run({ - task: 'Search for the top 10 Hacker News posts and return the title, url, and score', - schema: TaskOutput, - }); - - const posts = rsp.doneOutput?.posts; - - if (posts == null) { - throw new Error('No posts found'); - } - - for (const post of posts) { - console.log(`${post.title} (${post.score}) - ${post.url}`); - } - - console.log(`\nFound ${posts.length} posts`); -} - -main().catch(console.error); diff --git a/examples/run.ts b/examples/run.ts index 33982e5..d515892 100755 --- a/examples/run.ts +++ b/examples/run.ts @@ -1,6 +1,7 @@ #!/usr/bin/env -S npm run tsn -T import { BrowserUse } from 'browser-use-sdk'; +import { z } from 'zod'; import { env } from './utils'; @@ -9,15 +10,54 @@ env(); // gets API Key from environment variable BROWSER_USE_API_KEY const browseruse = new BrowserUse(); -async function main() { - console.log(`Running Task...`); +async function basic() { + console.log(`Basic: Running Task...`); // Create Task const rsp = await browseruse.tasks.run({ task: "What's the weather line in SF and what's the temperature?", + agentSettings: { llm: 'gemini-2.5-flash' }, }); - console.log(rsp.doneOutput); + console.log(`Basic: ${rsp.doneOutput}`); + + console.log(`Basic: DONE`); +} + +const HackerNewsResponse = z.object({ + title: z.string(), + url: z.string(), +}); + +const TaskOutput = z.object({ + posts: z.array(HackerNewsResponse), +}); + +async function structured() { + console.log(`Structured: Running Task...`); + + // Create Task + const rsp = await browseruse.tasks.run({ + task: 'Search for the top 10 Hacker News posts and return the title and url!', + schema: TaskOutput, + agentSettings: { llm: 'gpt-4.1' }, + }); + + const posts = rsp.parsedOutput?.posts; + + if (posts == null) { + throw new Error('Structured: No posts found'); + } + + console.log(`Structured: Top Hacker News posts:`); + + for (const post of posts) { + console.log(` - ${post.title} - ${post.url}`); + } + + console.log(`\nStructured: DONE`); } -main().catch(console.error); +basic() + .then(() => structured()) + .catch(console.error); diff --git a/examples/stream-zod.ts b/examples/stream-zod.ts deleted file mode 100755 index 8989bd7..0000000 --- a/examples/stream-zod.ts +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env -S npm run tsn -T - -import { BrowserUse } from 'browser-use-sdk'; -import z from 'zod'; - -import { env } from './utils'; - -env(); - -const HackerNewsResponse = z.object({ - title: z.string(), - url: z.string(), - score: z.number(), -}); - -const TaskOutput = z.object({ - posts: z.array(HackerNewsResponse), -}); - -async function main() { - // gets API Key from environment variable BROWSER_USE_API_KEY - const browseruse = new BrowserUse(); - - console.log('Creating task and starting stream...\n'); - - // Create a task and get the stream - const task = await browseruse.tasks.create({ - task: 'Extract top 10 Hacker News posts and return the title, url, and score', - schema: TaskOutput, - }); - - const stream = browseruse.tasks.stream({ - taskId: task.id, - schema: TaskOutput, - }); - - for await (const msg of stream) { - // Regular - process.stdout.write(`${msg.data.status}`); - if (msg.data.session.liveUrl) { - process.stdout.write(` | Live URL: ${msg.data.session.liveUrl}`); - } - - if (msg.data.steps.length > 0) { - const latestStep = msg.data.steps[msg.data.steps.length - 1]; - process.stdout.write(` | ${latestStep!.nextGoal}`); - } - - process.stdout.write('\n'); - - // Output - if (msg.data.status === 'finished') { - process.stdout.write(`\n\nOUTPUT:`); - - for (const post of msg.data.doneOutput!.posts) { - process.stdout.write(`\n - ${post.title} (${post.score}) ${post.url}`); - } - } - } - - console.log('\nStream completed'); -} - -main().catch(console.error); diff --git a/examples/stream.ts b/examples/stream.ts index 19d44ae..da9139f 100755 --- a/examples/stream.ts +++ b/examples/stream.ts @@ -3,27 +3,92 @@ import { BrowserUse } from 'browser-use-sdk'; import { env } from './utils'; +import z from 'zod'; env(); -async function main() { +async function basic() { // gets API Key from environment variable BROWSER_USE_API_KEY const browseruse = new BrowserUse(); - console.log('Creating task and starting stream...'); + console.log('Basic: Creating task and starting stream...'); // Create a task and get the stream const task = await browseruse.tasks.create({ task: 'What is the weather in San Francisco?', + agentSettings: { llm: 'gemini-2.5-flash' }, }); const gen = browseruse.tasks.stream(task.id); for await (const msg of gen) { - console.log(msg); + console.log( + `Basic: ${msg.data.status} ${msg.data.session.liveUrl} ${msg.data.steps[msg.data.steps.length - 1]?.nextGoal}`, + ); + + if (msg.data.status === 'finished') { + console.log(`Basic: ${msg.data.doneOutput}`); + } + } + + console.log('\nBasic: Stream completed'); +} + +const HackerNewsResponse = z.object({ + title: z.string(), + url: z.string(), + score: z.number(), +}); + +const TaskOutput = z.object({ + posts: z.array(HackerNewsResponse), +}); + +async function structured() { + // gets API Key from environment variable BROWSER_USE_API_KEY + const browseruse = new BrowserUse(); + + console.log('Structured: Creating task and starting stream...\n'); + + // Create a task and get the stream + const task = await browseruse.tasks.create({ + task: 'Extract top 10 Hacker News posts and return the title, url, and score', + schema: TaskOutput, + agentSettings: { llm: 'gpt-4.1' }, + }); + + const stream = browseruse.tasks.stream({ + taskId: task.id, + schema: TaskOutput, + }); + + for await (const msg of stream) { + // Regular + process.stdout.write(`Structured: ${msg.data.status}`); + if (msg.data.session.liveUrl) { + process.stdout.write(` | Live URL: ${msg.data.session.liveUrl}`); + } + + if (msg.data.steps.length > 0) { + const latestStep = msg.data.steps[msg.data.steps.length - 1]; + process.stdout.write(` | ${latestStep!.nextGoal}`); + } + + process.stdout.write('\n'); + + // Output + if (msg.data.status === 'finished') { + process.stdout.write(`\n\nOUTPUT:`); + + for (const post of msg.data.parsedOutput!.posts) { + process.stdout.write(`\n - ${post.title} (${post.score}) ${post.url}`); + } + } } - console.log('\nStream completed'); + console.log('\nStructured: Stream completed'); } -main().catch(console.error); +basic() + .then(() => structured()) + .catch(console.error); diff --git a/package.json b/package.json index b27e731..d9c0a9a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "browser-use-sdk", - "version": "1.1.0", + "version": "1.1.1", "description": "The official TypeScript library for the Browser Use API", "author": "Browser Use ", "types": "dist/index.d.ts", diff --git a/src/lib/parse.ts b/src/lib/parse.ts index f235edf..b01e92d 100644 --- a/src/lib/parse.ts +++ b/src/lib/parse.ts @@ -13,8 +13,8 @@ export function stringifyStructuredOutput(schema: T): string // RETRIEVE -export type TaskViewWithSchema = Omit & { - doneOutput: z.output | null; +export type TaskViewWithSchema = TaskView & { + parsedOutput: z.output | null; }; export function parseStructuredTaskOutput( @@ -22,7 +22,7 @@ export function parseStructuredTaskOutput( schema: T, ): TaskViewWithSchema { if (res.doneOutput == null) { - return { ...res, doneOutput: null }; + return { ...res, parsedOutput: null }; } try { @@ -33,12 +33,12 @@ export function parseStructuredTaskOutput( throw new Error(`Invalid structured output: ${response.error.message}`); } - return { ...res, doneOutput: response.data }; + return { ...res, parsedOutput: response.data }; } catch (e) { if (e instanceof SyntaxError) { return { ...res, - doneOutput: null, + parsedOutput: null, }; } throw e; diff --git a/src/lib/stream.ts b/src/lib/stream.ts index 7848a32..89f747a 100644 --- a/src/lib/stream.ts +++ b/src/lib/stream.ts @@ -1,86 +1,14 @@ -import type { TaskView, TaskStepView } from '../resources/tasks'; -import { type DeepMutable, ExhaustiveSwitchCheck } from './types'; - -export type ReducerEvent = TaskView | null; - -export type BrowserState = Readonly<{ - taskId: string; - sessionId: string; - - liveUrl: string | null; - steps: ReadonlyArray; - doneOutput: string | null; -}> | null; - -type BrowserAction = { - kind: 'status'; - status: TaskView; -}; - -export function reducer(state: BrowserState, action: BrowserAction): [BrowserState, ReducerEvent] { - switch (action.kind) { - case 'status': { - // INIT - - if (state == null) { - const liveUrl = action.status.session.liveUrl ?? null; - const doneOutput = action.status.doneOutput ?? null; - - const state: BrowserState = { - taskId: action.status.id, - sessionId: action.status.sessionId, - liveUrl: liveUrl, - steps: action.status.steps, - doneOutput: doneOutput, - }; - - return [state, action.status]; - } - - // UPDATE - - const liveUrl = action.status.session.liveUrl ?? state.liveUrl; - const doneOutput = action.status.doneOutput ?? state.doneOutput; - - const steps: TaskStepView[] = [...state.steps]; - if (action.status.steps != null) { - const newSteps = action.status.steps.slice(state.steps.length); - - for (const step of newSteps) { - steps.push(step); - } - } - - const newState: DeepMutable = { - ...state, - liveUrl, - steps, - doneOutput, - }; - - // CHANGES - - if ( - (state.liveUrl == null && liveUrl != null) || - state.steps.length !== steps.length || - state.doneOutput != doneOutput - ) { - const update: ReducerEvent = { - ...action.status, - steps: newState.steps, - session: { - ...action.status.session, - liveUrl: newState.liveUrl, - }, - doneOutput: newState.doneOutput, - }; - - return [newState, update]; - } - - return [newState, null]; - } - default: - throw new ExhaustiveSwitchCheck(action.kind); - } +import { createHash } from 'crypto'; +import stringify from 'fast-json-stable-stringify'; + +import type { TaskView } from '../resources'; + +/** + * Hashes the task view to detect changes. + * Uses fast-json-stable-stringify for deterministic JSON, then SHA-256. + */ +export function getTaskViewHash(view: TaskView): string { + const dump = stringify(view); + const hash = createHash('sha256').update(dump).digest('hex'); + return hash; } diff --git a/src/resources/tasks.ts b/src/resources/tasks.ts index cfffe12..0060bdf 100644 --- a/src/resources/tasks.ts +++ b/src/resources/tasks.ts @@ -12,8 +12,8 @@ import { type TaskCreateParamsWithSchema, type TaskViewWithSchema, } from '../lib/parse'; -import { BrowserState, reducer } from '../lib/stream'; -import * as TasksAPI from './tasks'; +import { getTaskViewHash } from '../lib/stream'; +import { ExhaustiveSwitchCheck } from '../lib/types'; export class Tasks extends APIResource { /** @@ -132,31 +132,34 @@ export class Tasks extends APIResource { config: { interval: number }, options?: RequestOptions, ): AsyncGenerator<{ event: 'status'; data: TaskView }> { - const tick: { current: number } = { current: 0 }; - const state: { current: BrowserState } = { current: null }; + const hash: { current: string | null } = { current: null }; poll: do { if (options?.signal?.aborted) { break poll; } - tick.current++; + const res = await this.retrieve(taskId); - const status = await this.retrieve(taskId); + const resHash = getTaskViewHash(res); - const [newState, event] = reducer(state.current, { kind: 'status', status }); + if (hash.current == null || resHash !== hash.current) { + hash.current = resHash; - if (event != null) { - yield { event: 'status', data: event }; + yield { event: 'status', data: res }; + } - if (event.status === 'finished') { + switch (res.status) { + case 'finished': + case 'stopped': + case 'paused': + break poll; + case 'started': + await new Promise((resolve) => setTimeout(resolve, config.interval)); break; - } + default: + throw new ExhaustiveSwitchCheck(res.status); } - - state.current = newState; - - await new Promise((resolve) => setTimeout(resolve, config.interval)); } while (true); } @@ -172,8 +175,6 @@ export class Tasks extends APIResource { body: string | { taskId: string; schema: ZodType }, options?: RequestOptions, ): AsyncGenerator { - let req: TaskCreateParams; - const taskId = typeof body === 'object' ? body.taskId : body; for await (const msg of this.watch(taskId, { interval: 500 }, options)) { diff --git a/src/version.ts b/src/version.ts index c80f975..4d7aaa3 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = '1.1.0'; // x-release-please-version +export const VERSION = '1.1.1'; // x-release-please-version