Compare commits

...

1 Commits

Author SHA1 Message Date
Nikhil Sonti
4c3b18b834 chore: clean up unused agent sdk 2026-04-28 17:13:56 -07:00
34 changed files with 59 additions and 3605 deletions

View File

@@ -42,7 +42,7 @@ jobs:
- name: Install dependencies
working-directory: packages/browseros-agent
run: bun install --ignore-scripts && bun run build:agent-sdk
run: bun install --ignore-scripts
- name: Install Python eval dependencies
run: pip install agisdk requests

View File

@@ -1,168 +1,11 @@
name: Release BrowserOS Agent SDK
name: Release BrowserOS Agent SDK (disabled)
on:
workflow_dispatch:
concurrency:
group: release-agent-sdk
cancel-in-progress: false
jobs:
publish:
if: github.ref == 'refs/heads/main'
disabled:
if: ${{ false }}
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
defaults:
run:
working-directory: packages/browseros-agent/packages/agent-sdk
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: oven-sh/setup-bun@v2
- uses: actions/setup-node@v6
with:
node-version: "20"
registry-url: "https://registry.npmjs.org"
- name: Install dependencies
run: bun ci
working-directory: packages/browseros-agent
- name: Build
run: bun run build
- name: Test
run: bun test
- name: Get version
id: version
run: |
echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"
echo "release_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Generate release notes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
SDK_PATH="packages/browseros-agent/packages/agent-sdk"
CURRENT_TAG="agent-sdk-v${{ steps.version.outputs.version }}"
# Find the previous tag, excluding the current version's tag
# (which may already exist from a prior failed run)
PREV_TAG=$(git tag -l "agent-sdk-v*" --sort=-v:refname | grep -v "^${CURRENT_TAG}$" | head -n 1)
if [ -z "$PREV_TAG" ]; then
echo "Initial release" > /tmp/release-notes.md
else
# Get commits scoped to the SDK directory
COMMITS=$(git log "$PREV_TAG"..HEAD --pretty=format:"%H" -- "$SDK_PATH")
if [ -z "$COMMITS" ]; then
echo "No notable changes." > /tmp/release-notes.md
else
echo "## What's Changed" > /tmp/release-notes.md
echo "" >> /tmp/release-notes.md
# For each commit, find the associated PR and format with author
CONTRIBUTORS=""
while IFS= read -r SHA; do
# Get commit subject and author
SUBJECT=$(git log -1 --pretty=format:"%s" "$SHA")
AUTHOR=$(git log -1 --pretty=format:"%an" "$SHA")
GITHUB_USER=$(gh api "/repos/${{ github.repository }}/commits/${SHA}" --jq '.author.login // empty' 2>/dev/null)
# Find associated PR number
PR_NUM=$(gh api "/repos/${{ github.repository }}/commits/${SHA}/pulls" --jq '.[0].number // empty' 2>/dev/null)
# Format line: skip PR number if already in the commit subject
# (squash merges include "(#123)" in the subject automatically)
if [ -n "$PR_NUM" ] && ! echo "$SUBJECT" | grep -qF "(#${PR_NUM})"; then
echo "- ${SUBJECT} (#${PR_NUM})" >> /tmp/release-notes.md
else
echo "- ${SUBJECT}" >> /tmp/release-notes.md
fi
done <<< "$COMMITS"
fi
fi
working-directory: ${{ github.workspace }}
- name: Publish
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Create GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="agent-sdk-v${{ steps.version.outputs.version }}"
RELEASE_SHA="${{ steps.version.outputs.release_sha }}"
TITLE="BrowserOS Agent SDK - v${{ steps.version.outputs.version }}"
# Create or reuse tag (idempotent for re-runs)
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "Tag $TAG already exists, skipping tag creation"
else
git tag "$TAG" "$RELEASE_SHA"
fi
# Push tag (skip if already on remote)
if git ls-remote --tags origin "$TAG" | grep -q "$TAG"; then
echo "Tag $TAG already on remote, skipping push"
else
git push origin "$TAG"
fi
# Create or update release
if gh release view "$TAG" >/dev/null 2>&1; then
echo "Release $TAG already exists, updating"
gh release edit "$TAG" --title "$TITLE" --notes-file /tmp/release-notes.md
else
gh release create "$TAG" --title "$TITLE" --notes-file /tmp/release-notes.md
fi
working-directory: ${{ github.workspace }}
- name: Update CHANGELOG.md via PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ steps.version.outputs.version }}"
DATE=$(date -u +"%Y-%m-%d")
BRANCH="docs/agent-sdk-changelog-v${VERSION}"
CHANGELOG="packages/browseros-agent/packages/agent-sdk/CHANGELOG.md"
# Return to main before branching
git checkout main
# Use head/tail to safely insert without sed quoting issues
{
head -n 1 "$CHANGELOG"
echo ""
echo "## v${VERSION} (${DATE})"
echo ""
cat /tmp/release-notes.md
echo ""
tail -n +2 "$CHANGELOG"
} > /tmp/new-changelog.md
mv /tmp/new-changelog.md "$CHANGELOG"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git checkout -b "$BRANCH"
git add "$CHANGELOG"
git commit -m "docs: update agent-sdk changelog for v${VERSION}"
git push origin "$BRANCH"
gh pr create \
--title "docs: update agent-sdk changelog for v${VERSION}" \
--body "Auto-generated changelog update for BrowserOS Agent SDK v${VERSION}." \
--base main \
--head "$BRANCH"
gh pr merge "$BRANCH" --squash --auto || true
working-directory: ${{ github.workspace }}
- run: echo "Agent SDK publishing is disabled."

View File

@@ -58,10 +58,6 @@ jobs:
command: (cd apps/server && bun run test:lib)
junit_path: test-results/server-lib.xml
needs_browser: false
- suite: server-sdk
command: (cd apps/server && bun run test:sdk)
junit_path: test-results/server-sdk.xml
needs_browser: true
- suite: server-root
command: (cd apps/server && bun run test:root)
junit_path: test-results/server-root.xml
@@ -74,10 +70,6 @@ jobs:
command: bun run test:eval
junit_path: test-results/eval.xml
needs_browser: false
- suite: agent-sdk
command: bun run test:agent-sdk
junit_path: test-results/agent-sdk.xml
needs_browser: false
- suite: build
command: bun run test:build
junit_path: test-results/build.xml

View File

@@ -1,6 +1,6 @@
# BrowserOS Agent
The agent platform powering [BrowserOS](https://github.com/browseros-ai/BrowserOS) — contains the MCP server, agent UI, CLI, evaluation framework, and SDK.
The agent platform powering [BrowserOS](https://github.com/browseros-ai/BrowserOS) — contains the MCP server, agent UI, CLI, and evaluation framework.
## Monorepo Structure
@@ -12,7 +12,6 @@ apps/
eval/ # Evaluation framework for benchmarking agents
packages/
agent-sdk/ # Node.js SDK (@browseros-ai/agent-sdk)
cdp-protocol/ # Type-safe Chrome DevTools Protocol bindings
shared/ # Shared constants (ports, timeouts, limits)
```
@@ -23,7 +22,6 @@ packages/
| `apps/agent` | Agent UI — Chrome extension for the chat interface |
| `apps/cli` | Go CLI — control BrowserOS from the terminal or AI coding agents |
| `apps/eval` | Benchmark framework — WebVoyager, Mind2Web evaluation |
| `packages/agent-sdk` | Node.js SDK for browser automation with natural language |
| `packages/cdp-protocol` | Auto-generated CDP type bindings used by the server |
| `packages/shared` | Shared constants used across packages |

View File

@@ -120,7 +120,6 @@ apps/server/
│ └── rpc.ts # JSON-RPC type definitions
├── tests/
│ ├── tools/ # Tool-level tests
│ ├── sdk/ # SDK integration tests
│ └── server.integration.test.ts
└── package.json
```
@@ -149,7 +148,6 @@ See the [agent monorepo README](../../README.md) for full environment variable r
```bash
bun run test:tools # Tool-level tests
bun run test:integration # Full integration tests (requires running BrowserOS)
bun run test:sdk # SDK integration tests
```
### Building

View File

@@ -20,7 +20,6 @@
"test:integration": "bun run ./tests/__helpers__/run-test-group.ts integration",
"test:lib": "bun run ./tests/__helpers__/run-test-group.ts lib",
"test:root": "bun run ./tests/__helpers__/run-test-group.ts root",
"test:sdk": "bun run ./tests/__helpers__/run-test-group.ts sdk",
"test:skills": "bun run ./tests/__helpers__/run-test-group.ts skills",
"test:tools": "bun run ./tests/__helpers__/run-test-group.ts tools",
"test:tools:acl": "bun run test:cleanup && bun --env-file=.env.development test ./tests/tools/acl-scorer.test.ts",
@@ -82,7 +81,6 @@
"@ai-sdk/openai": "^3.0.30",
"@ai-sdk/openai-compatible": "^2.0.30",
"@ai-sdk/provider": "^3.0.8",
"@browseros-ai/agent-sdk": "workspace:*",
"@browseros/cdp-protocol": "workspace:*",
"@browseros/shared": "workspace:*",
"@google/gemini-cli-core": "^0.16.0",

View File

@@ -1,267 +0,0 @@
/**
* @license
* Copyright 2025 BrowserOS
* SPDX-License-Identifier: AGPL-3.0-or-later
*
* SDK Routes - REST API for @browseros-ai/agent-sdk
*/
import { TIMEOUTS } from '@browseros/shared/constants/timeouts'
import { LLM_PROVIDERS } from '@browseros/shared/schemas/llm'
import { zValidator } from '@hono/zod-validator'
import { Hono } from 'hono'
import { stream } from 'hono/streaming'
import { logger } from '../../lib/logger'
import { BrowserService } from '../services/sdk/browser'
import { ChatService } from '../services/sdk/chat'
import { ExtractService } from '../services/sdk/extract'
import {
ActRequestSchema,
ExtractRequestSchema,
NavRequestSchema,
type SdkDeps,
SdkError,
VerifyRequestSchema,
} from '../services/sdk/types'
import { VerifyService } from '../services/sdk/verify'
import type { Env } from '../types'
import {
formatUIMessageStreamDone,
formatUIMessageStreamEvent,
} from '../utils/ui-message-stream'
async function waitForPageLoad(
browserService: BrowserService,
tabId: number,
): Promise<void> {
const startTime = Date.now()
while (Date.now() - startTime < TIMEOUTS.PAGE_LOAD_WAIT) {
const status = await browserService.getPageLoadStatus(tabId)
if (status.isPageComplete && status.isDOMContentLoaded) {
logger.debug('Page load complete', { tabId })
return
}
await new Promise((resolve) =>
setTimeout(resolve, TIMEOUTS.PAGE_LOAD_POLL_INTERVAL),
)
}
}
export function createSdkRoutes(deps: SdkDeps) {
const { port, browser, browserosId } = deps
const browserService = new BrowserService(browser)
const chatService = new ChatService(port)
const extractService = new ExtractService()
const verifyService = new VerifyService()
// Chain route definitions for proper Hono RPC type inference
return new Hono<Env>()
.post('/nav', zValidator('json', NavRequestSchema), async (c) => {
const { url, tabId, windowId } = c.req.valid('json')
logger.info('SDK nav request', { url, tabId, windowId })
try {
const { tabId: navigatedTabId } = await browserService.navigate(
url,
tabId,
windowId,
)
await waitForPageLoad(browserService, navigatedTabId)
return c.json({ success: true, tabId: navigatedTabId })
} catch (error) {
const err =
error instanceof SdkError
? error
: new SdkError(
error instanceof Error ? error.message : 'Navigation failed',
)
logger.error('SDK nav error', { url, error: err.message })
return c.json(
{ error: { message: err.message } },
err.statusCode as 400 | 500,
)
}
})
.post('/act', zValidator('json', ActRequestSchema), async (c) => {
const { instruction, context, browserContext, llm, sessionId } =
c.req.valid('json')
logger.info('SDK act request', {
instruction,
windowId: browserContext?.windowId,
hasSessionId: !!sessionId,
})
const llmConfig = llm ?? { provider: LLM_PROVIDERS.BROWSEROS }
if (llmConfig.provider !== LLM_PROVIDERS.BROWSEROS && !llmConfig.model) {
return c.json(
{
error: { message: 'model is required for non-browseros providers' },
},
400,
)
}
// Set SSE headers for Vercel AI stream
c.header('Content-Type', 'text/event-stream')
c.header('Cache-Control', 'no-cache')
c.header('Connection', 'keep-alive')
c.header('x-vercel-ai-ui-message-stream', 'v1')
return stream(c, async (honoStream) => {
try {
// Emit start event at route level
await honoStream.write(formatUIMessageStreamEvent({ type: 'start' }))
await chatService.executeAction({
instruction,
context,
browserContext,
llmConfig,
signal: c.req.raw.signal,
sessionId,
onSSEEvent: async (event) => {
// Events from AI agent are already properly formatted
// Skip start/finish (managed at route level), forward everything else
if (event.type === 'start' || event.type === 'finish') {
return
}
await honoStream.write(formatUIMessageStreamEvent(event))
},
})
// Emit finish at route level
await honoStream.write(
formatUIMessageStreamEvent({
type: 'finish',
finishReason: 'stop',
}),
)
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
await honoStream.write(
formatUIMessageStreamEvent({ type: 'abort' }),
)
await honoStream.write(formatUIMessageStreamDone())
return
}
const err =
error instanceof SdkError
? error
: new SdkError(
error instanceof Error
? error.message
: 'Action execution failed',
)
logger.error('SDK act error', { instruction, error: err.message })
await honoStream.write(
formatUIMessageStreamEvent({
type: 'error',
errorText: err.message,
}),
)
await honoStream.write(
formatUIMessageStreamEvent({
type: 'finish',
finishReason: 'error',
}),
)
} finally {
await honoStream.write(formatUIMessageStreamDone())
}
})
})
.post('/extract', zValidator('json', ExtractRequestSchema), async (c) => {
const {
instruction,
schema,
context,
windowId,
tabId: requestTabId,
} = c.req.valid('json')
logger.info('SDK extract request', {
instruction,
windowId,
tabId: requestTabId,
})
try {
// Use provided tabId, or get active tab (from window if specified)
const tabId =
requestTabId ?? (await browserService.getActiveTab(windowId)).tabId
const content = await browserService.getPageContent(tabId)
const data = await extractService.extract({
instruction,
schema,
content,
context,
})
return c.json({ data })
} catch (error) {
const err =
error instanceof SdkError
? error
: new SdkError(
error instanceof Error ? error.message : 'Extraction failed',
)
logger.error('SDK extract error', { instruction, error: err.message })
return c.json(
{ error: { message: err.message } },
err.statusCode as 400 | 500,
)
}
})
.post('/verify', zValidator('json', VerifyRequestSchema), async (c) => {
const {
expectation,
context,
windowId,
tabId: requestTabId,
llm,
} = c.req.valid('json')
logger.info('SDK verify request', {
expectation,
windowId,
tabId: requestTabId,
})
const llmConfig = llm ?? { provider: LLM_PROVIDERS.BROWSEROS }
try {
// Use provided tabId, or get active tab (from window if specified)
const tabId =
requestTabId ?? (await browserService.getActiveTab(windowId)).tabId
const [screenshot, interactiveElements] = await Promise.all([
browserService.getScreenshot(tabId),
browserService.getInteractiveElements(tabId, true),
])
const result = await verifyService.verify({
expectation,
screenshot,
interactiveElements: interactiveElements.content,
context,
llmConfig,
browserosId,
})
return c.json(result)
} catch (error) {
const err =
error instanceof SdkError
? error
: new SdkError(
error instanceof Error ? error.message : 'Verification failed',
)
logger.error('SDK verify error', { expectation, error: err.message })
return c.json(
{ error: { message: err.message } },
err.statusCode as 400 | 500,
)
}
})
}

View File

@@ -36,7 +36,6 @@ import { createOAuthRoutes } from './routes/oauth'
import { createOpenClawRoutes } from './routes/openclaw'
import { createProviderRoutes } from './routes/provider'
import { createRefinePromptRoutes } from './routes/refine-prompt'
import { createSdkRoutes } from './routes/sdk'
import { createShutdownRoute } from './routes/shutdown'
import { createSkillsRoutes } from './routes/skills'
import { createSoulRoutes } from './routes/soul'
@@ -199,14 +198,6 @@ export async function createHttpServer(config: HttpServerConfig) {
aiSdkDevtoolsEnabled: config.aiSdkDevtoolsEnabled,
}),
)
.route(
'/sdk',
createSdkRoutes({
port,
browser,
browserosId,
}),
)
.route('/agents', agentRoutes)
.route('/claw', clawRoutes)

View File

@@ -1,165 +0,0 @@
/**
* @license
* Copyright 2025 BrowserOS
* SPDX-License-Identifier: AGPL-3.0-or-later
*
* Browser Service - Direct browser operations for SDK
*/
import type { Browser, PageInfo } from '../../../browser/browser'
import type {
ActiveTab,
InteractiveElements,
NavigateResult,
PageLoadStatus,
Screenshot,
} from './types'
import { SdkError } from './types'
export class BrowserService {
constructor(private browser: Browser) {}
private selectPage(pages: PageInfo[], windowId?: number): PageInfo | null {
const scopedPages =
windowId === undefined
? pages
: pages.filter((page) => page.windowId === windowId)
if (scopedPages.length === 0) {
return null
}
return (
scopedPages.find((page) => page.isActive) ??
scopedPages.find((page) => !page.isHidden) ??
scopedPages[0]
)
}
private async findExistingPage(windowId?: number): Promise<PageInfo | null> {
if (windowId === undefined) {
const activePage = await this.browser.getActivePage()
if (activePage) {
return activePage
}
}
return this.selectPage(await this.browser.listPages(), windowId)
}
private async resolveExistingPage(windowId?: number): Promise<PageInfo> {
const page = await this.findExistingPage(windowId)
if (!page) {
throw new SdkError(
windowId === undefined
? 'No active tab found'
: 'No tab found in specified window',
)
}
return page
}
private async resolveNavigationPage(windowId?: number): Promise<PageInfo> {
const existingPage = await this.findExistingPage(windowId)
if (existingPage) {
return existingPage
}
if (windowId !== undefined) {
throw new SdkError('No tab found in specified window')
}
const pageId = await this.browser.newPage('about:blank', {
background: false,
})
const createdPage = (await this.browser.listPages()).find(
(page) => page.pageId === pageId,
)
if (!createdPage) {
throw new SdkError('Failed to create a tab for navigation')
}
return createdPage
}
private async getPageIdForTab(tabId: number): Promise<number> {
const resolved = await this.browser.resolveTabIds([tabId])
const pageId = resolved.get(tabId)
if (pageId === undefined) {
throw new SdkError(`Tab ${tabId} not found`, 404)
}
return pageId
}
async getActiveTab(windowId?: number): Promise<ActiveTab> {
const page = await this.resolveExistingPage(windowId)
return {
tabId: page.tabId,
url: page.url,
title: page.title,
windowId: page.windowId ?? 0,
}
}
async getPageContent(tabId: number): Promise<string> {
const pageId = await this.getPageIdForTab(tabId)
const content = await this.browser.contentAsMarkdown(pageId, {})
if (!content) {
throw new SdkError('No content found on page', 400)
}
return content
}
async getScreenshot(tabId: number): Promise<Screenshot> {
const pageId = await this.getPageIdForTab(tabId)
return await this.browser.screenshot(pageId, {
format: 'png',
fullPage: false,
})
}
async navigate(
url: string,
tabId?: number,
windowId?: number,
): Promise<NavigateResult> {
if (tabId !== undefined) {
const pages = await this.browser.listPages()
const page = pages.find((p) => p.tabId === tabId)
if (!page) {
throw new SdkError(`Tab ${tabId} not found`, 404)
}
await this.browser.goto(page.pageId, url)
return { tabId, windowId: page.windowId ?? 0 }
}
const activePage = await this.resolveNavigationPage(windowId)
await this.browser.goto(activePage.pageId, url)
return {
tabId: activePage.tabId,
windowId: activePage.windowId ?? 0,
}
}
async getPageLoadStatus(tabId: number): Promise<PageLoadStatus> {
const pages = await this.browser.listPages()
const page = pages.find((p) => p.tabId === tabId)
if (!page) {
throw new SdkError('Tab not found', 404)
}
return {
tabId: page.tabId,
isDOMContentLoaded: !page.isLoading,
isResourcesLoading: page.isLoading,
isPageComplete: !page.isLoading,
}
}
async getInteractiveElements(
tabId: number,
simplified = false,
_windowId?: number,
): Promise<InteractiveElements> {
const pageId = await this.getPageIdForTab(tabId)
const content = simplified
? await this.browser.snapshot(pageId)
: await this.browser.enhancedSnapshot(pageId)
return { content }
}
}

View File

@@ -1,168 +0,0 @@
/**
* @license
* Copyright 2025 BrowserOS
* SPDX-License-Identifier: AGPL-3.0-or-later
*
* Chat Service - Executes actions via /chat endpoint
*/
import type { BrowserContext } from '@browseros/shared/schemas/browser-context'
import type { LLMConfig } from '@browseros/shared/schemas/llm'
import type { UIMessageStreamEvent } from '@browseros/shared/schemas/ui-stream'
import { createParser, type EventSourceMessage } from 'eventsource-parser'
import { SdkError } from './types'
export interface ExecuteActionOptions {
instruction: string
context?: Record<string, unknown>
browserContext?: BrowserContext
llmConfig: LLMConfig
signal?: AbortSignal
onSSEEvent?: (event: UIMessageStreamEvent) => Promise<void>
/** If provided, reuses existing session. Caller is responsible for cleanup. */
sessionId?: string
}
export class ChatService {
private readonly chatUrl: string
constructor(port: number) {
this.chatUrl = `http://127.0.0.1:${port}/chat`
}
async executeAction(options: ExecuteActionOptions): Promise<void> {
const {
instruction,
context,
browserContext,
llmConfig,
signal,
onSSEEvent,
sessionId: providedSessionId,
} = options
if (signal?.aborted) {
throw new SdkError('Operation aborted', 400)
}
let message = instruction
if (context) {
message = `${instruction}\n\nContext:\n${JSON.stringify(context, null, 2)}`
}
// Use provided sessionId or generate ephemeral one
const isEphemeral = !providedSessionId
const conversationId = providedSessionId ?? crypto.randomUUID()
const response = await fetch(this.chatUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
conversationId,
message,
provider: llmConfig.provider,
model: llmConfig.model ?? 'default',
apiKey: llmConfig.apiKey,
baseUrl: llmConfig.baseUrl,
resourceName: llmConfig.resourceName,
region: llmConfig.region,
accessKeyId: llmConfig.accessKeyId,
secretAccessKey: llmConfig.secretAccessKey,
sessionToken: llmConfig.sessionToken,
browserContext,
}),
signal,
})
if (!response.ok) {
const errorText = await response.text()
throw new SdkError(
errorText || 'Chat request failed',
response.status >= 400 && response.status < 600 ? response.status : 500,
)
}
const reader = response.body?.getReader()
if (reader) {
try {
if (onSSEEvent) {
await this.parseAndForwardSSE(reader, signal, onSSEEvent)
} else {
await this.drainStream(reader, signal)
}
} finally {
reader.releaseLock()
}
}
// Clean up ephemeral sessions only (persistent sessions are managed by caller)
if (isEphemeral) {
await fetch(`${this.chatUrl}/${conversationId}`, {
method: 'DELETE',
}).catch(() => {})
}
}
private async parseAndForwardSSE(
reader: ReadableStreamDefaultReader<Uint8Array>,
signal: AbortSignal | undefined,
onSSEEvent: (event: UIMessageStreamEvent) => Promise<void>,
): Promise<void> {
const decoder = new TextDecoder()
const pendingEvents: UIMessageStreamEvent[] = []
const parser = createParser({
onEvent: (msg: EventSourceMessage) => {
if (msg.data === '[DONE]') return
try {
const event = JSON.parse(msg.data) as UIMessageStreamEvent
pendingEvents.push(event)
} catch {
// Invalid JSON, skip
}
},
})
while (true) {
if (signal?.aborted) {
await reader.cancel()
throw new SdkError('Operation aborted', 400)
}
const { done, value } = await reader.read()
if (done) break
const text = decoder.decode(value, { stream: true })
parser.feed(text)
// Process any events that were parsed
let event = pendingEvents.shift()
while (event) {
await onSSEEvent(event)
event = pendingEvents.shift()
}
}
// Process any remaining events
let remaining = pendingEvents.shift()
while (remaining) {
await onSSEEvent(remaining)
remaining = pendingEvents.shift()
}
}
private async drainStream(
reader: ReadableStreamDefaultReader<Uint8Array>,
signal: AbortSignal | undefined,
): Promise<void> {
while (true) {
if (signal?.aborted) {
await reader.cancel()
throw new SdkError('Operation aborted', 400)
}
const { done } = await reader.read()
if (done) break
}
}
}

View File

@@ -1,56 +0,0 @@
/**
* @license
* Copyright 2025 BrowserOS
* SPDX-License-Identifier: AGPL-3.0-or-later
*
* Extract Service - Structured data extraction via remote service
*/
import { EXTERNAL_URLS } from '@browseros/shared/constants/urls'
import { SdkError } from './types'
export interface ExtractOptions {
instruction: string
schema: Record<string, unknown>
content: string
context?: Record<string, unknown>
}
export interface ExtractResult {
data: unknown
}
export class ExtractService {
private serviceUrl: string
constructor() {
this.serviceUrl = `${EXTERNAL_URLS.CODEGEN_SERVICE}/api/extract`
}
async extract(options: ExtractOptions): Promise<unknown> {
const { instruction, schema, content, context } = options
const response = await fetch(this.serviceUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
instruction,
schema,
content,
context,
}),
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
const errorMessage =
(errorData as { error?: string }).error || 'Extraction service failed'
const status =
response.status >= 400 && response.status < 600 ? response.status : 500
throw new SdkError(errorMessage, status)
}
const result = (await response.json()) as ExtractResult
return result.data
}
}

View File

@@ -1,97 +0,0 @@
/**
* @license
* Copyright 2025 BrowserOS
* SPDX-License-Identifier: AGPL-3.0-or-later
*
* SDK Types - Type definitions and request schemas for SDK services
*/
import { BrowserContextSchema } from '@browseros/shared/schemas/browser-context'
import { LLMConfigSchema } from '@browseros/shared/schemas/llm'
import { z } from 'zod'
import type { Browser } from '../../../browser/browser'
// Request validation schemas
export const NavRequestSchema = z.object({
url: z.string().url(),
tabId: z.number().optional(),
windowId: z.number().optional(),
})
export const ActRequestSchema = z.object({
instruction: z.string().min(1),
context: z.record(z.unknown()).optional(),
maxSteps: z.number().optional(),
browserContext: BrowserContextSchema.optional(),
llm: LLMConfigSchema.optional(),
/** Session ID for state persistence. If provided, reuses existing session. */
sessionId: z.string().uuid().optional(),
})
export const ExtractRequestSchema = z.object({
instruction: z.string().min(1),
schema: z.record(z.unknown()),
context: z.record(z.unknown()).optional(),
windowId: z.number().optional(),
tabId: z.number().optional(),
})
export const VerifyRequestSchema = z.object({
expectation: z.string().min(1),
context: z.record(z.unknown()).optional(),
windowId: z.number().optional(),
tabId: z.number().optional(),
llm: LLMConfigSchema.optional(),
})
export type NavRequest = z.infer<typeof NavRequestSchema>
export type ActRequest = z.infer<typeof ActRequestSchema>
export type ExtractRequest = z.infer<typeof ExtractRequestSchema>
export type VerifyRequest = z.infer<typeof VerifyRequestSchema>
// Shared types
export interface SdkDeps {
port: number
browser: Browser
browserosId?: string
}
export interface ActiveTab {
tabId: number
url: string
title: string
windowId: number
}
export interface Screenshot {
data: string
mimeType: string
}
export interface InteractiveElements {
content: string
}
export interface NavigateResult {
tabId: number
windowId: number
}
export interface PageLoadStatus {
tabId: number
isDOMContentLoaded: boolean
isResourcesLoading: boolean
isPageComplete: boolean
}
export class SdkError extends Error {
constructor(
message: string,
public statusCode: number = 500,
) {
super(message)
this.name = 'SdkError'
}
}

View File

@@ -1,85 +0,0 @@
/**
* @license
* Copyright 2025 BrowserOS
* SPDX-License-Identifier: AGPL-3.0-or-later
*
* Verify Service - Page verification via LLM
*/
import type { LLMConfig } from '@browseros/shared/schemas/llm'
import type { ModelMessage } from 'ai'
import { LLMClient } from '../../../lib/clients/llm/client'
import type { Screenshot } from './types'
export interface VerifyOptions {
expectation: string
screenshot: Screenshot
pageContent?: string
interactiveElements?: string
context?: Record<string, unknown>
llmConfig: LLMConfig
browserosId?: string
}
export interface VerifyResult {
success: boolean
reason: string
}
export class VerifyService {
async verify(options: VerifyOptions): Promise<VerifyResult> {
const {
expectation,
screenshot,
pageContent,
interactiveElements,
context,
llmConfig,
browserosId,
} = options
const client = await LLMClient.create(llmConfig, browserosId)
let textPrompt = `Verify this expectation about the current page:
${expectation}
Look at the screenshot and any provided page data. Determine if the expectation is met.
Your response MUST start with exactly one of these words:
- SUCCESS - if the expectation is met
- FAILURE - if the expectation is NOT met
Then explain your reasoning.`
if (context) {
textPrompt += `\n\nAdditional context:\n${JSON.stringify(context, null, 2)}`
}
if (pageContent) {
textPrompt += `\n\nPage text content:\n${pageContent}`
}
if (interactiveElements) {
textPrompt += `\n\nInteractive elements:\n${interactiveElements}`
}
const imageUrl = `data:${screenshot.mimeType};base64,${screenshot.data}`
const messages: ModelMessage[] = [
{
role: 'user',
content: [
{ type: 'image', image: imageUrl },
{ type: 'text', text: textPrompt },
],
},
]
const response = await client.generateText(messages)
const trimmed = response.trim()
const success = /^SUCCESS\b/i.test(trimmed)
const reason = trimmed.replace(/^(SUCCESS|FAILURE)\s*/i, '').trim()
return { success, reason }
}
}

View File

@@ -6,14 +6,7 @@ const projectRoot = resolve(import.meta.dir, '..', '..')
const testsRoot = resolve(projectRoot, 'tests')
const cleanupScript = resolve(testsRoot, '__helpers__/cleanup.sh')
const testPreloadPath = './tests/__helpers__/test-env.ts'
const preferredDirectoryGroups = [
'agent',
'api',
'skills',
'tools',
'browser',
'sdk',
]
const preferredDirectoryGroups = ['agent', 'api', 'skills', 'tools', 'browser']
const ignoredDirectories = new Set(['__fixtures__', '__helpers__'])
const rootGroupExclusions = new Set(['server.integration.test.ts'])
const testFilePattern = /\.(test|spec)\.[cm]?[jt]sx?$/

View File

@@ -1,261 +0,0 @@
/**
* @license
* Copyright 2025 BrowserOS
*
* Integration tests for @browseros-ai/agent-sdk
* Tests the SDK against a real BrowserOS server.
*/
import { afterAll, beforeAll, describe, it } from 'bun:test'
import assert from 'node:assert'
import { Agent } from '@browseros-ai/agent-sdk'
import { CdpBackend } from '../../src/browser/backends/cdp'
import { Browser } from '../../src/browser/browser'
import {
ensureBrowserOS,
type TestEnvironmentConfig,
} from '../__helpers__/setup'
let config: TestEnvironmentConfig
let cdp: CdpBackend | null = null
let runtimeWindowId: number
async function getRuntimeWindow(
testConfig: TestEnvironmentConfig,
): Promise<number> {
const runtimeCdp = new CdpBackend({ port: testConfig.cdpPort })
await runtimeCdp.connect()
cdp = runtimeCdp
const browser = new Browser(runtimeCdp)
const pages = await browser.listPages()
const page =
pages.find((entry) => !entry.isHidden && entry.windowId !== undefined) ??
pages.find((entry) => entry.windowId !== undefined)
assert.ok(page?.windowId !== undefined, 'Expected a runtime window ID')
return page.windowId
}
beforeAll(async () => {
config = await ensureBrowserOS()
runtimeWindowId = await getRuntimeWindow(config)
}, 60000)
afterAll(async () => {
await cdp?.disconnect()
})
function createAgent(browserContext?: {
windowId?: number
activeTab?: { id: number; url: string }
}): Agent {
return new Agent({
url: `http://127.0.0.1:${config.serverPort}`,
browserContext,
})
}
describe('Agent SDK Integration', () => {
describe('nav()', () => {
it('navigates to a URL successfully', async () => {
const agent = createAgent()
const result = await agent.nav('https://google.com')
console.log('\n=== nav() Response ===')
console.log(JSON.stringify(result, null, 2))
assert.ok(result.success, 'Navigation should succeed')
}, 30000)
it('navigates to a data URL', async () => {
const agent = createAgent()
const result = await agent.nav('data:text/html,<h1>Test Page</h1>')
console.log('\n=== nav() Data URL Response ===')
console.log(JSON.stringify(result, null, 2))
assert.ok(result.success, 'Navigation to data URL should succeed')
}, 30000)
it('emits progress events', async () => {
const agent = createAgent()
const events: unknown[] = []
agent.onProgress((event) => events.push(event))
await agent.nav('https://example.com')
console.log('\n=== Progress Events ===')
console.log(JSON.stringify(events, null, 2))
assert.ok(events.length > 0, 'Should emit progress events')
assert.strictEqual(
(events[0] as { type: string }).type,
'start-step',
'First event should be start-step type',
)
// Check for nav-specific events (text events with id='nav')
const navEvents = events.filter(
(e) => (e as { id?: string }).id === 'nav',
)
assert.ok(navEvents.length > 0, 'Should emit nav-related events')
}, 30000)
it('handles invalid URL gracefully', async () => {
const agent = createAgent()
try {
await agent.nav('not-a-valid-url')
assert.fail('Should have thrown an error')
} catch (error) {
assert.ok(error instanceof Error, 'Should throw an error')
console.log('✓ Invalid URL rejected as expected')
}
}, 30000)
})
describe('act()', () => {
it('clicks a button on a test page', async () => {
const agent = createAgent()
// Navigate to a simple test page with a button
await agent.nav(
'data:text/html,<button id="btn" onclick="this.textContent=\'Clicked!\'">Click me</button>',
)
const result = await agent.act('click the button')
console.log('\n=== act() Response ===')
console.log(JSON.stringify(result, null, 2))
assert.ok(result.success, 'Action should succeed')
}, 60000)
it('emits progress events during action', async () => {
const agent = createAgent()
const events: unknown[] = []
agent.onProgress((event) => events.push(event))
await agent.nav('data:text/html,<h1>Test</h1>')
await agent.act('describe what you see')
console.log('\n=== act() Progress Events ===')
console.log(JSON.stringify(events, null, 2))
// act() emits SSE events like 'start', 'finish', 'text-delta', etc.
// The nav() events have id='nav', act() events come from the SSE stream
const actEvents = events.filter(
(e) =>
(e as { type: string }).type === 'start' ||
(e as { type: string }).type === 'finish',
)
assert.ok(actEvents.length > 0, 'Should emit act progress events')
}, 60000)
})
describe('extract()', () => {
it('extracts structured data from page', async () => {
const { z } = await import('zod')
const agent = createAgent()
await agent.nav(
'data:text/html,<h1>Welcome to My Site</h1><p>This is a test page.</p>',
)
const result = await agent.extract('get the page title', {
schema: z.object({ title: z.string() }),
})
console.log('\n=== extract() Response ===')
console.log(JSON.stringify(result, null, 2))
assert.ok(result.data, 'Should return extracted data')
assert.ok(
typeof result.data.title === 'string',
'Title should be a string',
)
}, 60000)
})
describe('verify()', () => {
it('verifies page state', async () => {
const agent = createAgent()
await agent.nav('data:text/html,<h1>Hello World</h1>')
const result = await agent.verify(
'the page contains a heading that says Hello World',
)
console.log('\n=== verify() Response ===')
console.log(JSON.stringify(result, null, 2))
assert.ok(
typeof result.success === 'boolean',
'Should return success boolean',
)
assert.ok(
typeof result.reason === 'string',
'Should return reason string',
)
}, 60000)
})
describe('browserContext', () => {
it('passes windowId through nav()', async () => {
const testWindowId = runtimeWindowId
const agent = createAgent({ windowId: testWindowId })
const events: unknown[] = []
agent.onProgress((event) => events.push(event))
const result = await agent.nav('data:text/html,<h1>Window Test</h1>')
console.log('\n=== nav() with windowId ===')
console.log('windowId:', testWindowId)
console.log('result:', JSON.stringify(result, null, 2))
assert.ok(
typeof result.success === 'boolean',
'Should return a result with success boolean',
)
}, 30000)
it('passes windowId through act()', async () => {
const testWindowId = runtimeWindowId
const agent = createAgent({ windowId: testWindowId })
const plainAgent = createAgent()
await plainAgent.nav('data:text/html,<button id="btn">Click</button>')
const result = await agent.act('describe what you see')
console.log('\n=== act() with windowId ===')
console.log('windowId:', testWindowId)
console.log('result:', JSON.stringify(result, null, 2))
assert.ok(
typeof result.success === 'boolean',
'Should return a result with success boolean',
)
}, 60000)
it('passes windowId through extract()', async () => {
const { z } = await import('zod')
const testWindowId = runtimeWindowId
const agent = createAgent({ windowId: testWindowId })
const plainAgent = createAgent()
await plainAgent.nav('data:text/html,<h1>Extract Test</h1>')
const result = await agent.extract('get the page heading', {
schema: z.object({ heading: z.string() }),
})
console.log('\n=== extract() with windowId ===')
console.log('windowId:', testWindowId)
console.log('result:', JSON.stringify(result, null, 2))
assert.ok(result.data, 'Should return extracted data')
}, 60000)
})
})

View File

@@ -170,7 +170,6 @@
"@ai-sdk/openai": "^3.0.30",
"@ai-sdk/openai-compatible": "^2.0.30",
"@ai-sdk/provider": "^3.0.8",
"@browseros-ai/agent-sdk": "workspace:*",
"@browseros/cdp-protocol": "workspace:*",
"@browseros/shared": "workspace:*",
"@google/gemini-cli-core": "^0.16.0",
@@ -217,22 +216,6 @@
"chrome-devtools-mcp": "latest",
},
},
"packages/agent-sdk": {
"name": "@browseros-ai/agent-sdk",
"version": "0.0.7",
"dependencies": {
"eventsource-parser": "^3.0.6",
"zod-to-json-schema": "^3.24.1",
},
"devDependencies": {
"@browseros/shared": "workspace:*",
"bunup": "^0.16.17",
"typedoc": "^0.28.15",
},
"peerDependencies": {
"zod": "^3.x",
},
},
"packages/build-tools": {
"name": "@browseros/build-tools",
"version": "0.0.0",
@@ -473,8 +456,6 @@
"@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.1", "", {}, "sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw=="],
"@browseros-ai/agent-sdk": ["@browseros-ai/agent-sdk@workspace:packages/agent-sdk"],
"@browseros/agent": ["@browseros/agent@workspace:apps/agent"],
"@browseros/build-tools": ["@browseros/build-tools@workspace:packages/build-tools"],
@@ -487,10 +468,6 @@
"@browseros/shared": ["@browseros/shared@workspace:packages/shared"],
"@bunup/dts": ["@bunup/dts@0.14.49", "", { "dependencies": { "@babel/parser": "^7.28.6", "coffi": "^0.1.37", "oxc-minify": "^0.93.0", "oxc-resolver": "^11.16.2", "oxc-transform": "^0.93.0", "picocolors": "^1.1.1", "std-env": "^3.10.0", "ts-import-resolver": "^0.1.23" }, "peerDependencies": { "typescript": ">=4.5.0" }, "optionalPeers": ["typescript"] }, "sha512-mAni3Zpf1MLxFDNCNStcxjbgNaSZpygc1qqF66WHMoUs3j+NSNKZPKwex9uLGXazJdE8g9MqQ//1QQcETdwY6Q=="],
"@bunup/shared": ["@bunup/shared@0.16.19", "", { "peerDependencies": { "typescript": "latest" }, "optionalPeers": ["typescript"] }, "sha512-gk0ALbDgEifVtZYRZ/ivvwF53pvOdpfqtLIIK67Q417nvtn/XPGE9NZLbdsowTKy2befmT8eiTxMXeoLyZPibA=="],
"@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@11.0.3", "", { "dependencies": { "@chevrotain/gast": "11.0.3", "@chevrotain/types": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ=="],
"@chevrotain/gast": ["@chevrotain/gast@11.0.3", "", { "dependencies": { "@chevrotain/types": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q=="],
@@ -589,12 +566,8 @@
"@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="],
"@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
"@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
"@emoji-mart/data": ["@emoji-mart/data@1.2.1", "", {}, "sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw=="],
"@emoji-mart/react": ["@emoji-mart/react@1.1.1", "", { "peerDependencies": { "emoji-mart": "^5.2", "react": "^16.8 || ^17 || ^18" } }, "sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g=="],
@@ -1105,8 +1078,6 @@
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
"@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="],
"@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="],
@@ -1227,106 +1198,6 @@
"@opentelemetry/sql-common": ["@opentelemetry/sql-common@0.41.2", "", { "dependencies": { "@opentelemetry/core": "^2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0" } }, "sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ=="],
"@oxc-minify/binding-android-arm64": ["@oxc-minify/binding-android-arm64@0.93.0", "", { "os": "android", "cpu": "arm64" }, "sha512-N3j/JoK4hXwQbnyOJoEltM8MEkddWV3XtfYimO6jsMjr5R6QdauKaSVeQHO/lSezB7SFkrMPqr6X7tBfghHiXA=="],
"@oxc-minify/binding-darwin-arm64": ["@oxc-minify/binding-darwin-arm64@0.93.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kLJJe7uBE+a9ql6eLGAtJ1g1LuEXi4aHbsiu342wGe+wRieSPi/Cx0aeDsQjdetwK5mqJWjWS2FO/n03jiw+IQ=="],
"@oxc-minify/binding-darwin-x64": ["@oxc-minify/binding-darwin-x64@0.93.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-0g6sVLaatgJpD28et/ykZCr5MHn7SWOblRCpXS47vcVmRyBFnUt21oiv3RqKacl1LKgk3czDHHribEfRyygiVw=="],
"@oxc-minify/binding-freebsd-x64": ["@oxc-minify/binding-freebsd-x64@0.93.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-h8HUtllzBHIx31Uxx1maxd7ur4lMi0qoz0OzoqiohZ1/Ty5GZDe3F/yd6mN4M913e4xrNlcGLFaN8tI9W+Fstw=="],
"@oxc-minify/binding-linux-arm-gnueabihf": ["@oxc-minify/binding-linux-arm-gnueabihf@0.93.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CIElV4OLqkt5PJtirJ1ZRNvKkELhPkDWZmYpiDRI43mQl6Hkt0YsoBAeFKqrwx3AfwAYWvbFjHII++S+Vvnc4A=="],
"@oxc-minify/binding-linux-arm-musleabihf": ["@oxc-minify/binding-linux-arm-musleabihf@0.93.0", "", { "os": "linux", "cpu": "arm" }, "sha512-AOvZfiwnNULzSHxtW0BO0VXrFzDDFcknLZzQLZF8z9i070un+0S2Q3oZtA13gx7Z7LFJ87+/gKmgSKjHUI1Mzg=="],
"@oxc-minify/binding-linux-arm64-gnu": ["@oxc-minify/binding-linux-arm64-gnu@0.93.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-RNBTRvQ3XiVwoDU12y92HCwkznRb5N5ybGqC1Jt/eyWuRDI2838rzcoTrHQ/oke9/u4vHZ1lZwabR8z+VALE1g=="],
"@oxc-minify/binding-linux-arm64-musl": ["@oxc-minify/binding-linux-arm64-musl@0.93.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qtE6qL7HC61rLKQBCvgsNJ9Bvvhe4U6V9VBPmvFw1SKRhoUp3PAzkWGnrvAgqQQnFaP34sUq/P4YmXmWL9m7bw=="],
"@oxc-minify/binding-linux-riscv64-gnu": ["@oxc-minify/binding-linux-riscv64-gnu@0.93.0", "", { "os": "linux", "cpu": "none" }, "sha512-2/8Y2lY8Klns2xrOufR26SR6Ci79NbOR89vuQJCjQwVS7Veb8Gk3lJWn6XRna2QxdvjMQ+6+kmW2/CajJED1rg=="],
"@oxc-minify/binding-linux-s390x-gnu": ["@oxc-minify/binding-linux-s390x-gnu@0.93.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-0TyiFgN1NPBQcKbdOh4VDt16kaFknKRXvmVshblEqp0JdwZeHFDCWVsTdpFH+NCbnUuzeY932IeP18dUnT/BVw=="],
"@oxc-minify/binding-linux-x64-gnu": ["@oxc-minify/binding-linux-x64-gnu@0.93.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2vc22dp1RtbfKUM3DbkOvEwGa11JsjAdLHXBAAAkNAqD/ux+ERwLscoP2O71I5jXMTbeHe6eDem6gldyiWr76Q=="],
"@oxc-minify/binding-linux-x64-musl": ["@oxc-minify/binding-linux-x64-musl@0.93.0", "", { "os": "linux", "cpu": "x64" }, "sha512-o7vVnDnF5k3xlgINjL7D2R9v47jczG5Jk+4xeZ+O9c9XxSx4jvGTqtBWf1Ka95fupJEm2QpSrO+wlA14exF3kA=="],
"@oxc-minify/binding-wasm32-wasi": ["@oxc-minify/binding-wasm32-wasi@0.93.0", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.0.5" }, "cpu": "none" }, "sha512-cW338ha926sD39uRDOrD/OMBgzosVtVPthXj1cl5rW9OQxuduXNqWbn/rJCA4X16T3IUcUnIbP2oCR8NI9D9xw=="],
"@oxc-minify/binding-win32-arm64-msvc": ["@oxc-minify/binding-win32-arm64-msvc@0.93.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-f/STdah7v3L6hZDcGUlk2NVcXtwlrzFUiQHI+TcE21uMzHItCEv34gkDtrAq04sAsDF+Aykuw3LnsWwg0pv7RA=="],
"@oxc-minify/binding-win32-x64-msvc": ["@oxc-minify/binding-win32-x64-msvc@0.93.0", "", { "os": "win32", "cpu": "x64" }, "sha512-FLnVjLRW3ifwHR6/87q/OQCukw+KTCR1SpcawwcykG0F62BQZjiwXpEHwNWgrV41M9Mdd52ND8/+HrMqcFlZMw=="],
"@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.16.3", "", { "os": "android", "cpu": "arm" }, "sha512-CVyWHu6ACDqDcJxR4nmGiG8vDF4TISJHqRNzac5z/gPQycs/QrP/1pDsJBy0MD7jSw8nVq2E5WqeHQKabBG/Jg=="],
"@oxc-resolver/binding-android-arm64": ["@oxc-resolver/binding-android-arm64@11.16.3", "", { "os": "android", "cpu": "arm64" }, "sha512-tTIoB7plLeh2o6Ay7NnV5CJb6QUXdxI7Shnsp2ECrLSV81k+oVE3WXYrQSh4ltWL75i0OgU5Bj3bsuyg5SMepw=="],
"@oxc-resolver/binding-darwin-arm64": ["@oxc-resolver/binding-darwin-arm64@11.16.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-OXKVH7uwYd3Rbw1s2yJZd6/w+6b01iaokZubYhDAq4tOYArr+YCS+lr81q1hsTPPRZeIsWE+rJLulmf1qHdYZA=="],
"@oxc-resolver/binding-darwin-x64": ["@oxc-resolver/binding-darwin-x64@11.16.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-WwjQ4WdnCxVYZYd3e3oY5XbV3JeLy9pPMK+eQQ2m8DtqUtbxnvPpAYC2Knv/2bS6q5JiktqOVJ2Hfia3OSo0/A=="],
"@oxc-resolver/binding-freebsd-x64": ["@oxc-resolver/binding-freebsd-x64@11.16.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-4OHKFGJBBfOnuJnelbCS4eBorI6cj54FUxcZJwEXPeoLc8yzORBoJ2w+fQbwjlQcUUZLEg92uGhKCRiUoqznjg=="],
"@oxc-resolver/binding-linux-arm-gnueabihf": ["@oxc-resolver/binding-linux-arm-gnueabihf@11.16.3", "", { "os": "linux", "cpu": "arm" }, "sha512-OM3W0NLt9u7uKwG/yZbeXABansZC0oZeDF1nKgvcZoRw4/Yak6/l4S0onBfDFeYMY94eYeAt2bl60e30lgsb5A=="],
"@oxc-resolver/binding-linux-arm-musleabihf": ["@oxc-resolver/binding-linux-arm-musleabihf@11.16.3", "", { "os": "linux", "cpu": "arm" }, "sha512-MRs7D7i1t7ACsAdTuP81gLZES918EpBmiUyEl8fu302yQB+4L7L7z0Ui8BWnthUTQd3nAU9dXvENLK/SqRVH8A=="],
"@oxc-resolver/binding-linux-arm64-gnu": ["@oxc-resolver/binding-linux-arm64-gnu@11.16.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-0eVYZxSceNqGADzhlV4ZRqkHF0fjWxRXQOB7Qwl5y1gN/XYUDvMfip+ngtzj4dM7zQT4U97hUhJ7PUKSy/JIGQ=="],
"@oxc-resolver/binding-linux-arm64-musl": ["@oxc-resolver/binding-linux-arm64-musl@11.16.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-B1BvLeZbgDdVN0FvU40l5Q7lej8310WlabCBaouk8jY7H7xbI8phtomTtk3Efmevgfy5hImaQJu6++OmcFb2NQ=="],
"@oxc-resolver/binding-linux-ppc64-gnu": ["@oxc-resolver/binding-linux-ppc64-gnu@11.16.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-q7khglic3Jqak7uDgA3MFnjDeI7krQT595GDZpvFq785fmFYSx8rlTkoHzmhQtUisYtl4XG7WUscwsoidFUI4w=="],
"@oxc-resolver/binding-linux-riscv64-gnu": ["@oxc-resolver/binding-linux-riscv64-gnu@11.16.3", "", { "os": "linux", "cpu": "none" }, "sha512-aFRNmQNPzDgQEbw2s3c8yJYRimacSDI+u9df8rn5nSKzTVitHmbEpZqfxpwNLCKIuLSNmozHR1z1OT+oZVeYqg=="],
"@oxc-resolver/binding-linux-riscv64-musl": ["@oxc-resolver/binding-linux-riscv64-musl@11.16.3", "", { "os": "linux", "cpu": "none" }, "sha512-vZI85SvSMADcEL9G1TIrV0Rlkc1fY5Mup0DdlVC5EHPysZB4hXXHpr+h09pjlK5y+5om5foIzDRxE1baUCaWOA=="],
"@oxc-resolver/binding-linux-s390x-gnu": ["@oxc-resolver/binding-linux-s390x-gnu@11.16.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-xiLBnaUlddFEzRHiHiSGEMbkg8EwZY6VD8F+3GfnFsiK3xg/4boaUV2bwXd+nUzl3UDQOMW1QcZJ4jJSb0qiJA=="],
"@oxc-resolver/binding-linux-x64-gnu": ["@oxc-resolver/binding-linux-x64-gnu@11.16.3", "", { "os": "linux", "cpu": "x64" }, "sha512-6y0b05wIazJJgwu7yU/AYGFswzQQudYJBOb/otDhiDacp1+6ye8egoxx63iVo9lSpDbipL++54AJQFlcOHCB+g=="],
"@oxc-resolver/binding-linux-x64-musl": ["@oxc-resolver/binding-linux-x64-musl@11.16.3", "", { "os": "linux", "cpu": "x64" }, "sha512-RmMgwuMa42c9logS7Pjprf5KCp8J1a1bFiuBFtG9/+yMu0BhY2t+0VR/um7pwtkNFvIQqAVh6gDOg/PnoKRcdQ=="],
"@oxc-resolver/binding-openharmony-arm64": ["@oxc-resolver/binding-openharmony-arm64@11.16.3", "", { "os": "none", "cpu": "arm64" }, "sha512-/7AYRkjjW7xu1nrHgWUFy99Duj4/ydOBVaHtODie9/M6fFngo+8uQDFFnzmr4q//sd/cchIerISp/8CQ5TsqIA=="],
"@oxc-resolver/binding-wasm32-wasi": ["@oxc-resolver/binding-wasm32-wasi@11.16.3", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-urM6aIPbi5di4BSlnpd/TWtDJgG6RD06HvLBuNM+qOYuFtY1/xPbzQ2LanBI2ycpqIoIZwsChyplALwAMdyfCQ=="],
"@oxc-resolver/binding-win32-arm64-msvc": ["@oxc-resolver/binding-win32-arm64-msvc@11.16.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-QuvLqGKf7frxWHQ5TnrcY0C/hJpANsaez99Q4dAk1hen7lDTD4FBPtBzPnntLFXeaVG3PnSmnVjlv0vMILwU7Q=="],
"@oxc-resolver/binding-win32-ia32-msvc": ["@oxc-resolver/binding-win32-ia32-msvc@11.16.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QR/witXK6BmYTlEP8CCjC5fxeG5U9A6a50pNpC1nLnhAcJjtzFG8KcQ5etVy/XvCLiDc7fReaAWRNWtCaIhM8Q=="],
"@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.16.3", "", { "os": "win32", "cpu": "x64" }, "sha512-bFuJRKOscsDAEZ/a8BezcTMAe2BQ/OBRfuMLFUuINfTR5qGVcm4a3xBIrQVepBaPxFj16SJdRjGe05vDiwZmFw=="],
"@oxc-transform/binding-android-arm64": ["@oxc-transform/binding-android-arm64@0.93.0", "", { "os": "android", "cpu": "arm64" }, "sha512-sjmbt7SbsIgHC9luOLgwoFTI2zbTDesZlfiSFrSYNZv6S6o4zfR2Q/OLhRQqmar15JtxP8NVPuiPyqyx0mqHyg=="],
"@oxc-transform/binding-darwin-arm64": ["@oxc-transform/binding-darwin-arm64@0.93.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XhYYdSU1Oz1UFeMm8fbfdPrODDQkLz2USDjKmfGuoOQRFKXlq0YTktfzF6z1bxn+T8pc9jIlBDr7f+cyy2CCjg=="],
"@oxc-transform/binding-darwin-x64": ["@oxc-transform/binding-darwin-x64@0.93.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cRQE9cWmP1cLPqGKqbr453nio1uIgv2LAfIfdx1fQSClG6PvzfTWTqujM0bJpKquodkm4k07ug35+tp0aIkl0w=="],
"@oxc-transform/binding-freebsd-x64": ["@oxc-transform/binding-freebsd-x64@0.93.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-m2vojbIELwBwX4ehbEs+7gXx4ooBn2mpR8MkxjZdhucMTj7P38W+jSdW+04pX+/bf2vYxP2madTEZXSX6mseLg=="],
"@oxc-transform/binding-linux-arm-gnueabihf": ["@oxc-transform/binding-linux-arm-gnueabihf@0.93.0", "", { "os": "linux", "cpu": "arm" }, "sha512-NEoI0t9b8NHzvXuBIADYubKUbfsuDsY9g/uOTiVNoP+r16vpOdiY3avoqP2x2WPSiuprYVFM3Olq3WVngSg+IA=="],
"@oxc-transform/binding-linux-arm-musleabihf": ["@oxc-transform/binding-linux-arm-musleabihf@0.93.0", "", { "os": "linux", "cpu": "arm" }, "sha512-gzhgsb/o+V2PBElu2aMD7H1ZYOntr4lzuXDyVq/RbwwzF3G3jjFMB5hddbcjky8rdtmVzEaqqESI2h5kWkZUAw=="],
"@oxc-transform/binding-linux-arm64-gnu": ["@oxc-transform/binding-linux-arm64-gnu@0.93.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-4PdTqvMzLeMLbkwpHvj2ovQoIKaK8i1OnUGW7XzhZKPBGhkcdt/H3oa5FhZ2uoqSIM1KnKKP80MSC1OYqK+w0Q=="],
"@oxc-transform/binding-linux-arm64-musl": ["@oxc-transform/binding-linux-arm64-musl@0.93.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-c+CrpmFv32Z1WfR2V8sEKPI4XLewK9hQUq57RUDXj3P99IZ9eA0qIq/2Azle4YGbHdeEywAvqEDlbGa7o3ZFNQ=="],
"@oxc-transform/binding-linux-riscv64-gnu": ["@oxc-transform/binding-linux-riscv64-gnu@0.93.0", "", { "os": "linux", "cpu": "none" }, "sha512-9rkciYe67g/uuVU4bFst96c7Xc2rk2fhzWTsBySUjTvxpgEeBXPsx78OLNwFVZoGf0lGNMXU/oSxr8OImEgvcw=="],
"@oxc-transform/binding-linux-s390x-gnu": ["@oxc-transform/binding-linux-s390x-gnu@0.93.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-b/3he7qO4It9bTZbKWNvYMVNyoNldgWVDsTleWiRSskDZPTP6CggpcxVolltn8Eegiq4GycKSN1riInTngR6+w=="],
"@oxc-transform/binding-linux-x64-gnu": ["@oxc-transform/binding-linux-x64-gnu@0.93.0", "", { "os": "linux", "cpu": "x64" }, "sha512-PeKWwubXPza6JGYjZGRt3sleTAaxTac4SG3Nd/VF9WGCY7ljAb6Q0t/gIuyjLm7tgB2E4luFezJogqkAW/b1ng=="],
"@oxc-transform/binding-linux-x64-musl": ["@oxc-transform/binding-linux-x64-musl@0.93.0", "", { "os": "linux", "cpu": "x64" }, "sha512-UjeqejYo3ynOimHKKPdqtryD0iCWiYHRnNNl5sxzK4GPA/JcxNnRGejAbLH6gkPOFtAi2k4Y5ujc2nU8cX8LSw=="],
"@oxc-transform/binding-wasm32-wasi": ["@oxc-transform/binding-wasm32-wasi@0.93.0", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.0.5" }, "cpu": "none" }, "sha512-pMUgg0Mm5ASd8oEPf/yiZmHCqH5WMC0mjCK3CccEvfpUf+WC8WYiKiLkPz+0e7AyPW/Kb+MDI9FaYDKQ5QgyoQ=="],
"@oxc-transform/binding-win32-arm64-msvc": ["@oxc-transform/binding-win32-arm64-msvc@0.93.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-RR30xNVMIEe5PWSD26uGEZp6yH7fkLKznvPSebVOVl2IWW8Sjnd59i6Ws08FmBKH9QP3jW30MypL6ESdlE5yWw=="],
"@oxc-transform/binding-win32-x64-msvc": ["@oxc-transform/binding-win32-x64-msvc@0.93.0", "", { "os": "win32", "cpu": "x64" }, "sha512-6QN3DEaEw3eWioWEFRgNsTvYq8czYSnpkjB2za+/WdLN0g5FzOl2ZEfNiPrBWIPnSmjUmDWtWVWcSjwY7fX5/Q=="],
"@parcel/watcher": ["@parcel/watcher@2.5.4", "", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.3" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.4", "@parcel/watcher-darwin-arm64": "2.5.4", "@parcel/watcher-darwin-x64": "2.5.4", "@parcel/watcher-freebsd-x64": "2.5.4", "@parcel/watcher-linux-arm-glibc": "2.5.4", "@parcel/watcher-linux-arm-musl": "2.5.4", "@parcel/watcher-linux-arm64-glibc": "2.5.4", "@parcel/watcher-linux-arm64-musl": "2.5.4", "@parcel/watcher-linux-x64-glibc": "2.5.4", "@parcel/watcher-linux-x64-musl": "2.5.4", "@parcel/watcher-win32-arm64": "2.5.4", "@parcel/watcher-win32-ia32": "2.5.4", "@parcel/watcher-win32-x64": "2.5.4" } }, "sha512-WYa2tUVV5HiArWPB3ydlOc4R2ivq0IDrlqhMi3l7mVsFEXNcTfxYFPIHXHXIh/ca/y/V5N4E1zecyxdIBjYnkQ=="],
"@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.4", "", { "os": "android", "cpu": "arm64" }, "sha512-hoh0vx4v+b3BNI7Cjoy2/B0ARqcwVNrzN/n7DLq9ZB4I3lrsvhrkCViJyfTj/Qi5xM9YFiH4AmHGK6pgH1ss7g=="],
@@ -1817,8 +1688,6 @@
"@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
@@ -2193,8 +2062,6 @@
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
"bunup": ["bunup@0.16.20", "", { "dependencies": { "@bunup/dts": "^0.14.49", "@bunup/shared": "0.16.19", "chokidar": "^5.0.0", "coffi": "^0.1.37", "lightningcss": "^1.30.2", "picocolors": "^1.1.1", "tinyexec": "^1.0.2", "tree-kill": "^1.2.2", "zlye": "^0.4.4" }, "peerDependencies": { "typescript": "latest" }, "optionalPeers": ["typescript"], "bin": { "bunup": "dist/cli/index.js" } }, "sha512-a3Bu6ho8Bs7YEynHzUvwBVXXJIbSxlYMDYZrndyIIL1waUYQ4Eh9V62vS8TpVESOxcx8mQfuS5Hb1bXYE2bYkA=="],
"byte-counter": ["byte-counter@0.1.0", "", {}, "sha512-jheRLVMeUKrDBjVw2O5+k4EvR4t9wtxHL+bo/LxfkxsVeuGMy3a5SEGgXdAFA4FSzTrU8rQXQIrsZ3oBq5a0pQ=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
@@ -2293,8 +2160,6 @@
"codemirror": ["codemirror@6.0.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/lint": "^6.0.0", "@codemirror/search": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0" } }, "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw=="],
"coffi": ["coffi@0.1.37", "", { "dependencies": { "strip-json-comments": "^5.0.3" } }, "sha512-ewO5Xis7sw7g54yI/3lJ/nNV90Er4ZnENeDORZjrs58T70MmwKFLZgevraNCz+RmB4KDKsYT1ui1wDB36iPWqQ=="],
"collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
@@ -3553,12 +3418,6 @@
"outvariant": ["outvariant@1.4.0", "", {}, "sha512-AlWY719RF02ujitly7Kk/0QlV+pXGFDHrHf9O2OKqyqgBieaPOIeuSkL8sRK6j2WK+/ZAURq2kZsY0d8JapUiw=="],
"oxc-minify": ["oxc-minify@0.93.0", "", { "optionalDependencies": { "@oxc-minify/binding-android-arm64": "0.93.0", "@oxc-minify/binding-darwin-arm64": "0.93.0", "@oxc-minify/binding-darwin-x64": "0.93.0", "@oxc-minify/binding-freebsd-x64": "0.93.0", "@oxc-minify/binding-linux-arm-gnueabihf": "0.93.0", "@oxc-minify/binding-linux-arm-musleabihf": "0.93.0", "@oxc-minify/binding-linux-arm64-gnu": "0.93.0", "@oxc-minify/binding-linux-arm64-musl": "0.93.0", "@oxc-minify/binding-linux-riscv64-gnu": "0.93.0", "@oxc-minify/binding-linux-s390x-gnu": "0.93.0", "@oxc-minify/binding-linux-x64-gnu": "0.93.0", "@oxc-minify/binding-linux-x64-musl": "0.93.0", "@oxc-minify/binding-wasm32-wasi": "0.93.0", "@oxc-minify/binding-win32-arm64-msvc": "0.93.0", "@oxc-minify/binding-win32-x64-msvc": "0.93.0" } }, "sha512-pwMjOGN/I+cfLVkSmECcVHROKwECNVAXCT5h/29S4f0aArIUh3CQnix1yYy7MTQ3yThNuGANjjE9jWJyT43Vbw=="],
"oxc-resolver": ["oxc-resolver@11.16.3", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.16.3", "@oxc-resolver/binding-android-arm64": "11.16.3", "@oxc-resolver/binding-darwin-arm64": "11.16.3", "@oxc-resolver/binding-darwin-x64": "11.16.3", "@oxc-resolver/binding-freebsd-x64": "11.16.3", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.16.3", "@oxc-resolver/binding-linux-arm-musleabihf": "11.16.3", "@oxc-resolver/binding-linux-arm64-gnu": "11.16.3", "@oxc-resolver/binding-linux-arm64-musl": "11.16.3", "@oxc-resolver/binding-linux-ppc64-gnu": "11.16.3", "@oxc-resolver/binding-linux-riscv64-gnu": "11.16.3", "@oxc-resolver/binding-linux-riscv64-musl": "11.16.3", "@oxc-resolver/binding-linux-s390x-gnu": "11.16.3", "@oxc-resolver/binding-linux-x64-gnu": "11.16.3", "@oxc-resolver/binding-linux-x64-musl": "11.16.3", "@oxc-resolver/binding-openharmony-arm64": "11.16.3", "@oxc-resolver/binding-wasm32-wasi": "11.16.3", "@oxc-resolver/binding-win32-arm64-msvc": "11.16.3", "@oxc-resolver/binding-win32-ia32-msvc": "11.16.3", "@oxc-resolver/binding-win32-x64-msvc": "11.16.3" } }, "sha512-goLOJH3x69VouGWGp5CgCIHyksmOZzXr36lsRmQz1APg3SPFORrvV2q7nsUHMzLVa6ZJgNwkgUSJFsbCpAWkCA=="],
"oxc-transform": ["oxc-transform@0.93.0", "", { "optionalDependencies": { "@oxc-transform/binding-android-arm64": "0.93.0", "@oxc-transform/binding-darwin-arm64": "0.93.0", "@oxc-transform/binding-darwin-x64": "0.93.0", "@oxc-transform/binding-freebsd-x64": "0.93.0", "@oxc-transform/binding-linux-arm-gnueabihf": "0.93.0", "@oxc-transform/binding-linux-arm-musleabihf": "0.93.0", "@oxc-transform/binding-linux-arm64-gnu": "0.93.0", "@oxc-transform/binding-linux-arm64-musl": "0.93.0", "@oxc-transform/binding-linux-riscv64-gnu": "0.93.0", "@oxc-transform/binding-linux-s390x-gnu": "0.93.0", "@oxc-transform/binding-linux-x64-gnu": "0.93.0", "@oxc-transform/binding-linux-x64-musl": "0.93.0", "@oxc-transform/binding-wasm32-wasi": "0.93.0", "@oxc-transform/binding-win32-arm64-msvc": "0.93.0", "@oxc-transform/binding-win32-x64-msvc": "0.93.0" } }, "sha512-QCwM2nMAWf4hEBehLVA2apllxdmmWLb5M0in9HwC2boaaFbP0QntbLy4hfRZGur2KKyEBErZbH9S5NYX8eHslg=="],
"p-cancelable": ["p-cancelable@4.0.1", "", {}, "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg=="],
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
@@ -4125,8 +3984,6 @@
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
"stdin-discarder": ["stdin-discarder@0.3.1", "", {}, "sha512-reExS1kSGoElkextOcPkel4NE99S0BWxjUHQeDFnR8S993JxpPX7KU4MNmO19NXhlJp+8dmdCbKQVNgLJh2teA=="],
"stream-events": ["stream-events@1.0.5", "", { "dependencies": { "stubs": "^3.0.0" } }, "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg=="],
@@ -4249,8 +4106,6 @@
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
"tree-sitter-bash": ["tree-sitter-bash@0.25.1", "", { "dependencies": { "node-addon-api": "^8.2.1", "node-gyp-build": "^4.8.2" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-7hMytuYIMoXOq24yRulgIxthE9YmggZIOHCyPTTuJcu6EU54tYD+4G39cUb28kxC6jMf/AbPfWGLQtgPTdh3xw=="],
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
@@ -4259,8 +4114,6 @@
"ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="],
"ts-import-resolver": ["ts-import-resolver@0.1.23", "", { "peerDependencies": { "typescript": ">=4.5.0" }, "optionalPeers": ["typescript"] }, "sha512-282pgr6j6aOvP3P2I6XugDxdBobkpdMmdbWjRjGl5gjPI1p0+oTNGDh1t924t75kRlyIkF65DiwhSIUysmyHQA=="],
"ts-log": ["ts-log@2.2.7", "", {}, "sha512-320x5Ggei84AxzlXp91QkIGSw5wgaLT6GeAH0KsqDmRZdVWW2OiSeVvElVoatk3f7nicwXlElXsoFkARiGE2yg=="],
"ts-md5": ["ts-md5@2.0.1", "", {}, "sha512-yF35FCoEOFBzOclSkMNEUbFQZuv89KEQ+5Xz03HrMSGUGB1+r+El+JiGOFwsP4p9RFNzwlrydYoTLvPOuICl9w=="],
@@ -4481,9 +4334,7 @@
"zip-dir": ["zip-dir@2.0.0", "", { "dependencies": { "async": "^3.2.0", "jszip": "^3.2.2" } }, "sha512-uhlsJZWz26FLYXOD6WVuq+fIcZ3aBPGo/cFdiLlv3KNwpa52IF3ISV8fLhQLiqVu5No3VhlqlgthN6gehil1Dg=="],
"zlye": ["zlye@0.4.4", "", { "dependencies": { "picocolors": "^1.1.1" }, "peerDependencies": { "typescript": ">=4.5.0" }, "optionalPeers": ["typescript"] }, "sha512-fwpeC841X3ElOLYRMKXbwX29pitNrsm6nRNvEhDMrRXDl3BhR2i03Bkr0GNrpyYgZJuEzUsBylXAYzgGPXXOCQ=="],
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="],
"zod-from-json-schema": ["zod-from-json-schema@0.1.0", "", { "dependencies": { "zod": "^3.24.2" } }, "sha512-yPcDmFziXePHO8iK4WXl4WcqlKolohQKEdhbJadCzZ/+/ayr1IELxWrVQ/NSUKiuIweNiDlZoi6dgle4Bd1baw=="],
@@ -4501,8 +4352,6 @@
"@ant-design/cssinjs-utils/@ant-design/cssinjs": ["@ant-design/cssinjs@1.24.0", "", { "dependencies": { "@babel/runtime": "^7.11.1", "@emotion/hash": "^0.8.0", "@emotion/unitless": "^0.7.5", "classnames": "^2.3.1", "csstype": "^3.1.3", "rc-util": "^5.35.0", "stylis": "^4.3.4" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-K4cYrJBsgvL+IoozUXYjbT6LHHNt+19a9zkvpBPxLjFHas1UpPM2A5MlhROb0BT8N8WoavM5VsP9MeSeNK/3mg=="],
"@anthropic-ai/claude-agent-sdk/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="],
"@aws-crypto/crc32/@aws-sdk/types": ["@aws-sdk/types@3.972.0", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-U7xBIbLSetONxb2bNzHyDgND3oKGoIfmknrEVnoEU4GUSs+0augUOIn9DIWGUO2ETcRFdsRUnmx9KhPT9Ojbug=="],
"@aws-crypto/sha1-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
@@ -4519,20 +4368,24 @@
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@better-auth/core/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="],
"@browseros/agent/@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
"@browseros/agent/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="],
"@browseros/build-tools/@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.1014.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.23", "@aws-sdk/credential-provider-node": "^3.972.24", "@aws-sdk/middleware-bucket-endpoint": "^3.972.8", "@aws-sdk/middleware-expect-continue": "^3.972.8", "@aws-sdk/middleware-flexible-checksums": "^3.974.3", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-location-constraint": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", "@aws-sdk/middleware-sdk-s3": "^3.972.23", "@aws-sdk/middleware-ssec": "^3.972.8", "@aws-sdk/middleware-user-agent": "^3.972.24", "@aws-sdk/region-config-resolver": "^3.972.9", "@aws-sdk/signature-v4-multi-region": "^3.996.11", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.10", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/eventstream-serde-browser": "^4.2.12", "@smithy/eventstream-serde-config-resolver": "^4.3.12", "@smithy/eventstream-serde-node": "^4.2.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-blob-browser": "^4.2.13", "@smithy/hash-node": "^4.2.12", "@smithy/hash-stream-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/md5-js": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-retry": "^4.4.44", "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.43", "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "@smithy/util-waiter": "^4.2.13", "tslib": "^2.6.2" } }, "sha512-0XLrOT4Cm3NEhhiME7l/8LbTXS4KdsbR4dSrY207KNKTcHLLTZ9EXt4ZpgnTfLvWQF3pGP2us4Zi1fYLo0N+Ow=="],
"@browseros/eval/@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.1014.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.23", "@aws-sdk/credential-provider-node": "^3.972.24", "@aws-sdk/middleware-bucket-endpoint": "^3.972.8", "@aws-sdk/middleware-expect-continue": "^3.972.8", "@aws-sdk/middleware-flexible-checksums": "^3.974.3", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-location-constraint": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", "@aws-sdk/middleware-sdk-s3": "^3.972.23", "@aws-sdk/middleware-ssec": "^3.972.8", "@aws-sdk/middleware-user-agent": "^3.972.24", "@aws-sdk/region-config-resolver": "^3.972.9", "@aws-sdk/signature-v4-multi-region": "^3.996.11", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.10", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/eventstream-serde-browser": "^4.2.12", "@smithy/eventstream-serde-config-resolver": "^4.3.12", "@smithy/eventstream-serde-node": "^4.2.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-blob-browser": "^4.2.13", "@smithy/hash-node": "^4.2.12", "@smithy/hash-stream-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/md5-js": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-retry": "^4.4.44", "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.43", "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "@smithy/util-waiter": "^4.2.13", "tslib": "^2.6.2" } }, "sha512-0XLrOT4Cm3NEhhiME7l/8LbTXS4KdsbR4dSrY207KNKTcHLLTZ9EXt4ZpgnTfLvWQF3pGP2us4Zi1fYLo0N+Ow=="],
"@browseros/eval/@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
"@browseros/eval/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@browseros/server/@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
"@browseros/server/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
"@browseros/server/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@browseros/shared/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@codesandbox/sandpack-react/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
"@devicefarmer/adbkit/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="],
@@ -4569,6 +4422,8 @@
"@google/gemini-cli-core/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
"@google/gemini-cli-core/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@google/genai/google-auth-library": ["google-auth-library@10.5.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", "gcp-metadata": "^8.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" } }, "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w=="],
"@graphql-codegen/add/tslib": ["tslib@2.6.2", "", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="],
@@ -4633,6 +4488,8 @@
"@graphql-tools/wrap/@graphql-tools/utils": ["@graphql-tools/utils@11.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA=="],
"@hono/zod-validator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
@@ -4641,6 +4498,38 @@
"@jimp/core/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="],
"@jimp/plugin-blit/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-circle/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-color/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-contain/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-cover/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-crop/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-displace/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-fisheye/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-flip/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-mask/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-print/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-quantize/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-resize/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-rotate/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-threshold/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/types/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@lexical/react/react-error-boundary": ["react-error-boundary@3.1.4", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "react": ">=16.13.1" } }, "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA=="],
"@lobehub/fluent-emoji/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
@@ -4661,8 +4550,6 @@
"@mdxeditor/editor/downshift": ["downshift@7.6.2", "", { "dependencies": { "@babel/runtime": "^7.14.8", "compute-scroll-into-view": "^2.0.4", "prop-types": "^15.7.2", "react-is": "^17.0.2", "tslib": "^2.3.0" }, "peerDependencies": { "react": ">=16.12.0" } }, "sha512-iOv+E1Hyt3JDdL9yYcOgW7nZ7GQ2Uz6YbggwXvKUSleetYhU2nXD482Rz6CzvM4lvI1At34BYruKAL4swRGxaA=="],
"@modelcontextprotocol/sdk/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="],
"@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/core": ["@opentelemetry/core@2.0.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw=="],
"@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.203.0", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/otlp-transformer": "0.203.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Wbxf7k+87KyvxFr5D7uOiSq/vHXWommvdnNE7vECO3tAhsA2GfOlpWINCMWUEPdHZ7tCXxw6Epp3vgx3jU7llQ=="],
@@ -4949,8 +4836,6 @@
"babel-plugin-macros/cosmiconfig": ["cosmiconfig@7.1.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="],
"better-auth/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="],
"boxen/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"boxen/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
@@ -4963,6 +4848,8 @@
"chrome-launcher/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
"chromium-bidi/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"cli-truncate/string-width": ["string-width@8.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg=="],
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
@@ -5109,6 +4996,8 @@
"openai/formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="],
"openai/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"ora/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"ora/is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
@@ -5145,8 +5034,6 @@
"publish-browser-extension/listr2": ["listr2@8.3.3", "", { "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ=="],
"publish-browser-extension/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="],
"rc/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
"rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
@@ -5235,6 +5122,8 @@
"wxt/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
"zod-from-json-schema/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@ai-sdk/amazon-bedrock/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="],
"@aklinker1/rollup-plugin-visualizer/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="],

View File

@@ -9,7 +9,6 @@
"packages/*"
],
"scripts": {
"postinstall": "bun run build:agent-sdk",
"dev:watch": "./tools/dev/run.sh watch",
"dev:watch:new": "./tools/dev/run.sh watch --new",
"dev:manual": "./tools/dev/run.sh watch --manual",
@@ -27,18 +26,15 @@
"start:server:test": "bun run build:server:test && set -a && . apps/server/.env.development && set +a && dist/prod/server/.tmp/binaries/browseros-server-darwin-arm64",
"build:agent:dev": "FORCE_COLOR=1 bun run --filter @browseros/agent --elide-lines=0 build:dev",
"build:agent": "bun run codegen:agent && bun run --filter @browseros/agent build",
"build:agent-sdk": "bun run --filter @browseros-ai/agent-sdk build",
"codegen:agent": "bun run --filter @browseros/agent codegen",
"test": "bun run test:all",
"test:all": "bun run test:server && bun run test:agent && bun run test:eval && bun run test:agent-sdk && bun run test:build",
"test:all": "bun run test:server && bun run test:agent && bun run test:eval && bun run test:build",
"test:server": "bun run --filter @browseros/server test",
"test:tools": "bun run --filter @browseros/server test:tools",
"test:cdp": "bun run --filter @browseros/server test:cdp",
"test:integration": "bun run --filter @browseros/server test:integration",
"test:sdk": "bun run --filter @browseros/server test:sdk",
"test:agent": "bun run ./scripts/run-bun-test.ts ./apps/agent",
"test:eval": "bun run ./scripts/run-bun-test.ts ./apps/eval/tests",
"test:agent-sdk": "bun run ./scripts/run-bun-test.ts ./packages/agent-sdk",
"test:build": "bun run ./scripts/run-bun-test.ts ./scripts/build",
"typecheck": "bun run --filter '*' typecheck",
"lint": "bunx biome check",

View File

@@ -1,12 +0,0 @@
# @browseros-ai/agent-sdk
## v0.0.7 (2026-03-26)
## What's Changed
- chore: bump @browseros-ai/agent-sdk to 0.0.7 (#569) (#569) @DaniAkash
## Contributors
- @DaniAkash

View File

@@ -1,208 +0,0 @@
# @browseros-ai/agent-sdk
[![npm version](https://img.shields.io/npm/v/@browseros-ai/agent-sdk)](https://www.npmjs.com/package/@browseros-ai/agent-sdk)
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](../../../../LICENSE)
Browser automation SDK for BrowserOS — navigate, interact, extract data, and verify page state using natural language.
Build automations that describe *what* to do, not *how* to do it. The SDK connects to a running BrowserOS instance and translates natural language instructions into browser actions using your choice of LLM provider.
## Prerequisites
- A running [BrowserOS](https://browseros.com) instance
- An API key for at least one [supported LLM provider](#llm-providers)
## Installation
```bash
npm install @browseros-ai/agent-sdk
# or
bun add @browseros-ai/agent-sdk
```
## Quick Start
```typescript
import { Agent } from '@browseros-ai/agent-sdk'
import { z } from 'zod'
const agent = new Agent({
url: 'http://localhost:9100',
llm: {
provider: 'openai',
apiKey: process.env.OPENAI_API_KEY,
},
})
// Navigate to a page
await agent.nav('https://example.com')
// Perform actions with natural language
await agent.act('click the login button')
// Extract structured data
const { data } = await agent.extract('get all product names and prices', {
schema: z.array(z.object({
name: z.string(),
price: z.number(),
})),
})
// Verify page state
const { success, reason } = await agent.verify('user is logged in')
```
## Multi-Step Example
Combine navigation, actions, extraction, and verification for end-to-end automation:
```typescript
import { Agent } from '@browseros-ai/agent-sdk'
import { z } from 'zod'
const agent = new Agent({
url: 'http://localhost:9100',
llm: { provider: 'anthropic', apiKey: process.env.ANTHROPIC_API_KEY },
})
// 1. Navigate
await agent.nav('https://news.ycombinator.com')
// 2. Extract data
const { data: stories } = await agent.extract('get the top 5 stories with title, points, and link', {
schema: z.array(z.object({
title: z.string(),
points: z.number(),
link: z.string(),
})),
})
// 3. Act on extracted data
await agent.act(`click on the story titled "${stories[0].title}"`)
// 4. Verify the result
const { success } = await agent.verify('the story page or external link has loaded')
console.log({ stories, navigationSuccess: success })
```
## API Reference
### `new Agent(options)`
Create a new agent instance.
```typescript
const agent = new Agent({
url: string, // BrowserOS server URL
llm?: LLMConfig, // Optional LLM configuration
onProgress?: (event) => void, // Progress callback
})
```
### `agent.nav(url, options?)`
Navigate to a URL.
```typescript
const { success } = await agent.nav('https://google.com')
```
### `agent.act(instruction, options?)`
Perform browser actions using natural language.
```typescript
// Simple action
await agent.act('click the submit button')
// With context interpolation
await agent.act('search for {{query}}', {
context: { query: 'browseros' },
})
// Multi-step with limit
await agent.act('fill out the form and submit', {
maxSteps: 15,
})
```
### `agent.extract(instruction, options)`
Extract structured data from the page.
```typescript
import { z } from 'zod'
const { data } = await agent.extract('get the page title', {
schema: z.object({ title: z.string() }),
})
```
### `agent.verify(expectation, options?)`
Verify the current page state.
```typescript
const { success, reason } = await agent.verify('the form was submitted successfully')
```
## LLM Providers
| Provider | Config |
|----------|--------|
| OpenAI | `{ provider: 'openai', apiKey: '...' }` |
| Anthropic | `{ provider: 'anthropic', apiKey: '...' }` |
| Google | `{ provider: 'google', apiKey: '...' }` |
| Azure | `{ provider: 'azure', apiKey: '...', resourceName: '...' }` |
| OpenRouter | `{ provider: 'openrouter', apiKey: '...' }` |
| Ollama | `{ provider: 'ollama', baseUrl: 'http://localhost:11434' }` |
| LM Studio | `{ provider: 'lmstudio', baseUrl: 'http://localhost:1234' }` |
| AWS Bedrock | `{ provider: 'bedrock', region: '...', accessKeyId: '...' }` |
| OpenAI Compatible | `{ provider: 'openai-compatible', baseUrl: '...', apiKey: '...' }` |
## Progress Events
Track agent operations in real time:
```typescript
const agent = new Agent({
url: 'http://localhost:9100',
onProgress: (event) => {
console.log(`[${event.type}] ${event.message}`)
},
})
```
Event types: `nav`, `act`, `extract`, `verify`, `error`, `done`
## Error Handling
```typescript
import {
NavigationError,
ActionError,
ExtractionError,
VerificationError,
ConnectionError
} from '@browseros-ai/agent-sdk'
try {
await agent.act('click non-existent button')
} catch (error) {
if (error instanceof ActionError) {
console.error('Action failed:', error.message)
}
}
```
## Links
- [Documentation](https://docs.browseros.com)
- [GitHub](https://github.com/browseros-ai/BrowserOS)
- [Changelog](./CHANGELOG.md)
- [Discord](https://discord.gg/YKwjt5vuKr)
## License
[AGPL-3.0-or-later](../../../../LICENSE)

View File

@@ -1,12 +0,0 @@
import { defineConfig } from 'bunup'
export default defineConfig({
entry: ['src/index.ts'],
outDir: 'dist',
format: ['esm', 'cjs'],
dts: {
resolve: true,
},
clean: true,
noExternal: ['@browseros/shared'],
})

View File

@@ -1,65 +0,0 @@
{
"name": "@browseros-ai/agent-sdk",
"version": "0.0.7",
"description": "Browser automation SDK for BrowserOS - navigate, interact, extract data with natural language",
"type": "module",
"license": "AGPL-3.0-or-later",
"repository": {
"type": "git",
"url": "https://github.com/browseros-ai/BrowserOS.git",
"directory": "packages/agent-sdk"
},
"homepage": "https://github.com/browseros-ai/BrowserOS#readme",
"bugs": {
"url": "https://github.com/browseros-ai/BrowserOS/issues"
},
"keywords": [
"browseros",
"browser-automation",
"web-scraping",
"ai-agent",
"natural-language",
"sdk"
],
"scripts": {
"build": "bunup && typedoc --json dist/api.json src/index.ts --excludePrivate",
"test": "bun test",
"typecheck": "tsc --noEmit",
"prepublishOnly": "bun run build"
},
"main": "./src/index.ts",
"module": "./src/index.ts",
"types": "./src/index.ts",
"files": [
"dist"
],
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
},
"./api.json": "./dist/api.json"
},
"publishConfig": {
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./api.json": "./dist/api.json"
}
},
"dependencies": {
"eventsource-parser": "^3.0.6",
"zod-to-json-schema": "^3.24.1"
},
"peerDependencies": {
"zod": "^3.x"
},
"devDependencies": {
"@browseros/shared": "workspace:*",
"bunup": "^0.16.17",
"typedoc": "^0.28.15"
}
}

View File

@@ -1,213 +0,0 @@
import type { AgentContext } from './context'
import { act } from './methods/act'
import { extract } from './methods/extract'
import { nav } from './methods/nav'
import { verify } from './methods/verify'
import type {
ActOptions,
ActResult,
AgentOptions,
BrowserContext,
ExtractOptions,
ExtractResult,
LLMConfig,
NavOptions,
NavResult,
UIMessageStreamEvent,
VerifyOptions,
VerifyResult,
} from './types'
/**
* Browser automation agent for the BrowserOS platform.
* Provides high-level methods to navigate, interact, extract data, and verify page state.
*
* @remarks
* The Agent instance is injected by the runtime - never instantiate it directly.
* Export a `run` function that receives the agent as a parameter.
*
* @example
* ```typescript
* import type { Agent } from '@browseros-ai/agent-sdk'
* import { z } from 'zod'
*
* export async function run(agent: Agent) {
* await agent.nav('https://example.com')
* await agent.act('click the login button')
* const { data } = await agent.extract('get page title', {
* schema: z.object({ title: z.string() })
* })
* return { message: 'Done', data }
* }
* ```
*/
export class Agent implements AsyncDisposable, AgentContext {
readonly baseUrl: string
readonly llmConfig?: LLMConfig
readonly signal?: AbortSignal
readonly stateful: boolean
private _browserContext?: BrowserContext
private progressCallback?: (event: UIMessageStreamEvent) => void
private _sessionId: string | null = null
private _disposed = false
constructor(options: AgentOptions) {
this.baseUrl = options.url.replace(/\/$/, '')
this.llmConfig = options.llm
this.progressCallback = options.onProgress
this.signal = options.signal
this._browserContext = options.browserContext
this.stateful = options.stateful ?? true
if (this.stateful) {
this._sessionId = crypto.randomUUID()
}
}
get browserContext(): BrowserContext | undefined {
return this._browserContext
}
get sessionId(): string | null {
return this._sessionId
}
set sessionId(value: string | null) {
this._sessionId = value
}
async dispose(): Promise<void> {
if (this._disposed) return
this._disposed = true
if (this._sessionId) {
await fetch(`${this.baseUrl}/chat/${this._sessionId}`, {
method: 'DELETE',
}).catch(() => {})
}
}
async [Symbol.asyncDispose](): Promise<void> {
await this.dispose()
}
throwIfAborted(): void {
if (this.signal?.aborted) {
throw new Error('Operation aborted')
}
}
onProgress(callback: (event: UIMessageStreamEvent) => void): void {
this.progressCallback = callback
}
emit(event: UIMessageStreamEvent): void {
this.progressCallback?.(event)
}
updateNavigationContext(
result: { tabId: number; windowId?: number },
url: string,
): void {
const nextWindowId = result.windowId ?? this._browserContext?.windowId
this._browserContext = {
...this._browserContext,
...(nextWindowId !== undefined ? { windowId: nextWindowId } : {}),
activeTab: {
id: result.tabId,
url,
},
}
}
/**
* Navigate to a URL and wait for the page to load.
*
* @param url - The URL to navigate to (must be a valid HTTP/HTTPS URL)
* @param options - Optional navigation settings
* @returns Promise resolving to `{ success: boolean }`
* @throws {NavigationError} When navigation fails
*
* @example
* ```typescript
* const { success } = await agent.nav('https://google.com')
* ```
*/
nav(url: string, options?: NavOptions): Promise<NavResult> {
return nav(this, url, options)
}
/**
* Perform a browser action described in natural language.
*
* @param instruction - Natural language description of the action
* @param options - Optional action settings including optional verification
* @returns Promise resolving to `{ success: boolean, steps: ActStep[] }`
* @throws {ActionError} When the action fails
*
* @example
* ```typescript
* // Simple action
* await agent.act('click the login button')
*
* // With verification and retry
* await agent.act('Click Add to Cart', {
* verify: 'Cart shows 1 item',
* maxRetries: 2
* })
*
* // With context interpolation
* await agent.act('search for {{query}}', {
* context: { query: 'wireless headphones' }
* })
* ```
*/
act(instruction: string, options?: ActOptions): Promise<ActResult> {
return act(this, instruction, options)
}
/**
* Extract structured data from the current page using natural language.
*
* @param instruction - Natural language description of what data to extract
* @param options - Extraction options with Zod schema
* @returns Promise resolving to `{ data: T }`
* @throws {ExtractionError} When extraction fails
*
* @example
* ```typescript
* import { z } from 'zod'
*
* const { data } = await agent.extract('get product info', {
* schema: z.object({
* name: z.string(),
* price: z.number()
* })
* })
* ```
*/
extract<T>(
instruction: string,
options: ExtractOptions<T>,
): Promise<ExtractResult<T>> {
return extract(this, instruction, options)
}
/**
* Verify that the current page matches an expected state.
*
* @param expectation - Natural language description of expected state
* @param options - Optional verification settings
* @returns Promise resolving to `{ success: boolean, reason: string }`
* @throws {VerificationError} When verification cannot be performed
*
* @example
* ```typescript
* const { success, reason } = await agent.verify('login form is visible')
* ```
*/
verify(expectation: string, options?: VerifyOptions): Promise<VerifyResult> {
return verify(this, expectation, options)
}
}

View File

@@ -1,25 +0,0 @@
import type { BrowserContext, LLMConfig, UIMessageStreamEvent } from './types'
/**
* Context interface that method modules use to access agent state and utilities.
* The Agent class implements this interface.
*/
export interface AgentContext {
readonly baseUrl: string
readonly llmConfig?: LLMConfig
readonly browserContext?: BrowserContext
readonly signal?: AbortSignal
readonly stateful: boolean
sessionId: string | null
emit(event: UIMessageStreamEvent): void
throwIfAborted(): void
updateNavigationContext(
result: {
tabId: number
windowId?: number
},
url: string,
): void
}

View File

@@ -1,67 +0,0 @@
/**
* Base error class for all Agent SDK errors.
* All SDK errors extend this class.
*/
export class AgentSDKError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly statusCode?: number,
) {
super(message)
this.name = 'AgentSDKError'
}
}
/**
* Thrown when the agent cannot connect to the BrowserOS runtime.
*/
export class ConnectionError extends AgentSDKError {
constructor(
message: string,
public readonly url: string,
) {
super(message, 'CONNECTION_ERROR')
this.name = 'ConnectionError'
}
}
/**
* Thrown when `nav()` fails to navigate to the target URL.
*/
export class NavigationError extends AgentSDKError {
constructor(message: string, statusCode?: number) {
super(message, 'NAVIGATION_ERROR', statusCode)
this.name = 'NavigationError'
}
}
/**
* Thrown when `act()` fails to perform the requested action.
*/
export class ActionError extends AgentSDKError {
constructor(message: string, statusCode?: number) {
super(message, 'ACTION_ERROR', statusCode)
this.name = 'ActionError'
}
}
/**
* Thrown when `extract()` fails to extract data or data doesn't match schema.
*/
export class ExtractionError extends AgentSDKError {
constructor(message: string, statusCode?: number) {
super(message, 'EXTRACTION_ERROR', statusCode)
this.name = 'ExtractionError'
}
}
/**
* Thrown when `verify()` encounters an error during verification.
*/
export class VerificationError extends AgentSDKError {
constructor(message: string, statusCode?: number) {
super(message, 'VERIFICATION_ERROR', statusCode)
this.name = 'VerificationError'
}
}

View File

@@ -1,28 +0,0 @@
export { Agent } from './agent'
export {
ActionError,
AgentSDKError,
ConnectionError,
ExtractionError,
NavigationError,
VerificationError,
} from './errors'
export type {
ActOptions,
ActResult,
ActStep,
AgentOptions,
BrowserContext,
ExtractOptions,
ExtractResult,
LLMConfig,
LLMProvider,
NavOptions,
NavResult,
ProgressEvent,
ProgressEventType,
ToolCall,
UIMessageStreamEvent,
VerifyOptions,
VerifyResult,
} from './types'

View File

@@ -1,136 +0,0 @@
import type { AgentContext } from '../context'
import { ActionError, ConnectionError } from '../errors'
import type { ActOptions, ActResult } from '../types'
import { parseSSEStream } from '../utils/sse-parser'
import { verifyInternal } from './verify'
/**
* Execute the action via SSE stream
*/
async function executeAct(
ctx: AgentContext,
instruction: string,
options?: ActOptions,
): Promise<ActResult> {
ctx.throwIfAborted()
const url = `${ctx.baseUrl}/sdk/act`
const browserContext =
options?.windowId === undefined
? ctx.browserContext
: {
...(ctx.browserContext ?? {}),
windowId: options.windowId,
}
let response: Response
try {
response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
instruction,
context: options?.context,
maxSteps: options?.maxSteps,
browserContext,
llm: ctx.llmConfig,
sessionId: ctx.sessionId,
}),
signal: ctx.signal,
})
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
throw new Error('Operation aborted')
}
throw new ConnectionError(
`Failed to connect to server: ${error instanceof Error ? error.message : String(error)}`,
url,
)
}
if (!response.ok) {
let errorMessage = `Request failed with status ${response.status}`
try {
const errorBody = await response.json()
if (errorBody.error?.message) {
errorMessage = errorBody.error.message
}
} catch {
// Use default error message
}
throw new ActionError(errorMessage, response.status)
}
const reader = response.body?.getReader()
const result: ActResult = { success: true, steps: [] }
if (reader) {
await parseSSEStream(ctx, reader, result)
}
return result
}
export async function act(
ctx: AgentContext,
instruction: string,
options?: ActOptions,
): Promise<ActResult> {
// Handle resetState on first call only
if (options?.resetState && ctx.stateful) {
ctx.sessionId = crypto.randomUUID()
}
// No verification - just execute
if (!options?.verify) {
return executeAct(ctx, instruction, options)
}
// With verification: execute + verify + retry loop
const maxRetries = options.maxRetries ?? 1
let lastResult: ActResult | null = null
let lastVerifyReason: string | undefined
for (let attempt = 0; attempt <= maxRetries; attempt++) {
// On retry, include the verification failure in the instruction
const retryInstruction =
attempt === 0 || !lastVerifyReason
? instruction
: `${instruction}\n\n[Previous attempt failed verification: "${lastVerifyReason}"]`
// Don't reset state on retries - let agent see previous context
const attemptOptions =
attempt === 0 ? options : { ...options, resetState: false }
lastResult = await executeAct(ctx, retryInstruction, attemptOptions)
// Action failed - no point verifying
if (!lastResult.success) {
return lastResult
}
// Verify the result
const verifyResult = await verifyInternal(ctx, options.verify)
if (verifyResult.success) {
return lastResult
}
lastVerifyReason = verifyResult.reason
// Emit retry info if more attempts remain
if (attempt < maxRetries) {
ctx.emit({
type: 'text-delta',
id: 'act-retry',
delta: `Verification failed: ${verifyResult.reason}. Retrying (${attempt + 1}/${maxRetries})...\n`,
})
}
}
// All retries exhausted - verification failed
return {
success: false,
steps: lastResult?.steps ?? [],
}
}

View File

@@ -1,44 +0,0 @@
import { zodToJsonSchema } from 'zod-to-json-schema'
import type { AgentContext } from '../context'
import { ExtractionError } from '../errors'
import type { ExtractOptions, ExtractResult } from '../types'
import { request } from '../utils/request'
export async function extract<T>(
ctx: AgentContext,
instruction: string,
options: ExtractOptions<T>,
): Promise<ExtractResult<T>> {
ctx.emit({ type: 'start-step' })
ctx.emit({ type: 'text-start', id: 'extract' })
ctx.emit({
type: 'text-delta',
id: 'extract',
delta: `Extracting: ${instruction}...\n`,
})
const jsonSchema = zodToJsonSchema(options.schema)
const result = await request<ExtractResult<T>>(
ctx,
'/sdk/extract',
{
instruction,
schema: jsonSchema,
context: options.context,
windowId: ctx.browserContext?.windowId,
tabId: ctx.browserContext?.activeTab?.id,
},
ExtractionError,
)
ctx.emit({
type: 'text-delta',
id: 'extract',
delta: 'Extraction complete.\n',
})
ctx.emit({ type: 'text-end', id: 'extract' })
ctx.emit({ type: 'finish-step' })
return result
}

View File

@@ -1,44 +0,0 @@
import type { AgentContext } from '../context'
import { NavigationError } from '../errors'
import type { NavOptions, NavResult } from '../types'
import { request } from '../utils/request'
export async function nav(
ctx: AgentContext,
url: string,
options?: NavOptions,
): Promise<NavResult> {
ctx.emit({ type: 'start-step' })
ctx.emit({ type: 'text-start', id: 'nav' })
ctx.emit({
type: 'text-delta',
id: 'nav',
delta: `Navigating to ${url}...\n`,
})
const windowId = options?.windowId ?? ctx.browserContext?.windowId
const result = await request<NavResult>(
ctx,
'/sdk/nav',
{ url, windowId, tabId: options?.tabId },
NavigationError,
)
if (result.success && result.tabId !== undefined) {
ctx.updateNavigationContext(
{ tabId: result.tabId, windowId: result.windowId },
url,
)
}
ctx.emit({
type: 'text-delta',
id: 'nav',
delta: result.success ? 'Navigation complete.\n' : 'Navigation failed.\n',
})
ctx.emit({ type: 'text-end', id: 'nav' })
ctx.emit({ type: 'finish-step' })
return result
}

View File

@@ -1,63 +0,0 @@
import type { AgentContext } from '../context'
import { VerificationError } from '../errors'
import type { VerifyOptions, VerifyResult } from '../types'
import { request } from '../utils/request'
export async function verify(
ctx: AgentContext,
expectation: string,
options?: VerifyOptions,
): Promise<VerifyResult> {
ctx.emit({ type: 'start-step' })
ctx.emit({ type: 'text-start', id: 'verify' })
ctx.emit({
type: 'text-delta',
id: 'verify',
delta: `Verifying: ${expectation}...\n`,
})
const result = await request<VerifyResult>(
ctx,
'/sdk/verify',
{
expectation,
context: options?.context,
windowId: ctx.browserContext?.windowId,
tabId: ctx.browserContext?.activeTab?.id,
llm: ctx.llmConfig,
},
VerificationError,
)
ctx.emit({
type: 'text-delta',
id: 'verify',
delta: result.success
? `Verification passed: ${result.reason}\n`
: `Verification failed: ${result.reason}\n`,
})
ctx.emit({ type: 'text-end', id: 'verify' })
ctx.emit({ type: 'finish-step' })
return result
}
/**
* Internal verify for act() retry loop - no UI events emitted
*/
export async function verifyInternal(
ctx: AgentContext,
expectation: string,
): Promise<VerifyResult> {
return request<VerifyResult>(
ctx,
'/sdk/verify',
{
expectation,
windowId: ctx.browserContext?.windowId,
tabId: ctx.browserContext?.activeTab?.id,
llm: ctx.llmConfig,
},
VerificationError,
)
}

View File

@@ -1,172 +0,0 @@
import type { BrowserContext } from '@browseros/shared/schemas/browser-context'
import type { LLMConfig, LLMProvider } from '@browseros/shared/schemas/llm'
import type { UIMessageStreamEvent } from '@browseros/shared/schemas/ui-stream'
import type { ZodSchema } from 'zod'
// Re-export shared types for consumers (bundled at build time)
export type { BrowserContext, LLMConfig, LLMProvider, UIMessageStreamEvent }
/**
* Configuration options for creating an Agent instance.
* @internal Used by runtime - not needed in generated code
*/
export interface AgentOptions {
url: string
llm?: LLMConfig
/** Browser context for targeting specific windows/tabs and MCP servers */
browserContext?: BrowserContext
/** Callback for streaming UI events (Vercel AI SDK format) */
onProgress?: (event: UIMessageStreamEvent) => void
signal?: AbortSignal
/**
* Enable stateful mode where conversation history persists across act() calls.
* When true, the agent "remembers" previous interactions.
* @default true
*/
stateful?: boolean
}
/**
* Options for the `nav()` method.
*/
export interface NavOptions {
/** Target a specific tab by ID */
tabId?: number
/** Target a specific window by ID */
windowId?: number
}
/**
* Options for the `act()` method.
*/
export interface ActOptions {
/** Key-value pairs to interpolate into the instruction using `{{key}}` syntax */
context?: Record<string, unknown>
/** Maximum number of steps for multi-step actions (default: 10) */
maxSteps?: number
/** Target a specific window by ID */
windowId?: number
/**
* Reset conversation state for this act() call.
* Starts fresh and continues with the new state for subsequent calls.
* @default false
*/
resetState?: boolean
/**
* Condition to verify after action succeeds.
* If verification fails, the action is retried up to `maxRetries` times.
* @example 'Cart shows 1 item'
*/
verify?: string
/**
* Maximum retry attempts when verification fails.
* Only used when `verify` is set.
* @default 1
*/
maxRetries?: number
}
/**
* Options for the `extract()` method.
*/
export interface ExtractOptions<T> {
/** Zod schema defining the expected data structure */
schema: ZodSchema<T>
/** Optional key-value pairs for additional context */
context?: Record<string, unknown>
}
/**
* Options for the `verify()` method.
*/
export interface VerifyOptions {
/** Optional key-value pairs for additional context */
context?: Record<string, unknown>
}
/**
* Types of progress events emitted by agent methods.
*/
export type ProgressEventType =
| 'nav'
| 'act'
| 'extract'
| 'verify'
| 'error'
| 'done'
/**
* Progress event emitted during agent operations.
*/
export interface ProgressEvent {
/** The type of operation */
type: ProgressEventType
/** Human-readable description of the current operation */
message: string
/** Additional metadata about the operation */
metadata?: Record<string, unknown>
}
/**
* Result returned by `nav()`.
*/
export interface NavResult {
/** Whether navigation succeeded */
success: boolean
/** The tab that was navigated */
tabId?: number
/** The window containing the navigated tab */
windowId?: number
}
/**
* Result returned by `act()`.
*/
export interface ActResult {
/** Whether the action succeeded */
success: boolean
/** The steps executed to complete the action */
steps: ActStep[]
}
/**
* A single step executed during an `act()` call.
*/
export interface ActStep {
/** The agent's reasoning for this step */
thought?: string
/** Tool calls made during this step */
toolCalls?: ToolCall[]
}
/**
* A tool call made during action execution.
*/
export interface ToolCall {
/** Name of the tool that was called */
name: string
/** Arguments passed to the tool */
args: Record<string, unknown>
/** Result returned by the tool */
result?: unknown
/** Error message if the tool call failed */
error?: string
}
/**
* Result returned by `extract()`.
*/
export interface ExtractResult<T> {
/** The extracted data matching the provided schema */
data: T
}
/**
* Result returned by `verify()`.
*/
export interface VerifyResult {
/** Whether the verification passed */
success: boolean
/** Explanation of why verification passed or failed */
reason: string
}

View File

@@ -1,52 +0,0 @@
import type { AgentContext } from '../context'
import type { AgentSDKError } from '../errors'
import { ConnectionError } from '../errors'
type ErrorConstructor = new (
message: string,
statusCode?: number,
) => AgentSDKError
export async function request<T>(
ctx: AgentContext,
endpoint: string,
body: Record<string, unknown>,
ErrorClass: ErrorConstructor,
): Promise<T> {
ctx.throwIfAborted()
const url = `${ctx.baseUrl}${endpoint}`
let response: Response
try {
response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
signal: ctx.signal,
})
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
throw new Error('Operation aborted')
}
throw new ConnectionError(
`Failed to connect to server: ${error instanceof Error ? error.message : String(error)}`,
url,
)
}
if (!response.ok) {
let errorMessage = `Request failed with status ${response.status}`
try {
const errorBody = await response.json()
if (errorBody.error?.message) {
errorMessage = errorBody.error.message
}
} catch {
// Use default error message
}
throw new ErrorClass(errorMessage, response.status)
}
return response.json() as Promise<T>
}

View File

@@ -1,87 +0,0 @@
import { createParser, type EventSourceMessage } from 'eventsource-parser'
import type { AgentContext } from '../context'
import type {
ActResult,
ActStep,
ToolCall,
UIMessageStreamEvent,
} from '../types'
export async function parseSSEStream(
ctx: AgentContext,
reader: ReadableStreamDefaultReader<Uint8Array>,
result: ActResult,
): Promise<void> {
const decoder = new TextDecoder()
const pendingEvents: UIMessageStreamEvent[] = []
let currentText = ''
const currentToolCalls = new Map<string, ToolCall>()
const parser = createParser({
onEvent: (msg: EventSourceMessage) => {
if (msg.data === '[DONE]') return
try {
const event = JSON.parse(msg.data) as UIMessageStreamEvent
pendingEvents.push(event)
} catch {
// Invalid JSON, skip
}
},
})
const processEvent = (event: UIMessageStreamEvent) => {
ctx.emit(event)
if (event.type === 'start-step') {
currentText = ''
currentToolCalls.clear()
} else if (event.type === 'text-delta') {
currentText += event.delta
} else if (event.type === 'tool-input-available') {
currentToolCalls.set(event.toolCallId, {
name: event.toolName,
args: event.input as Record<string, unknown>,
})
} else if (event.type === 'tool-output-available') {
const tc = currentToolCalls.get(event.toolCallId)
if (tc) tc.result = event.output
} else if (event.type === 'finish-step') {
const step: ActStep = {}
if (currentText) step.thought = currentText
if (currentToolCalls.size > 0) {
step.toolCalls = Array.from(currentToolCalls.values())
}
result.steps.push(step)
} else if (event.type === 'error') {
result.success = false
}
}
try {
while (true) {
ctx.throwIfAborted()
const { done, value } = await reader.read()
if (done) break
const text = decoder.decode(value, { stream: true })
parser.feed(text)
let event = pendingEvents.shift()
while (event) {
processEvent(event)
event = pendingEvents.shift()
}
}
let remaining = pendingEvents.shift()
while (remaining) {
processEvent(remaining)
remaining = pendingEvents.shift()
}
} finally {
reader.releaseLock()
}
}

View File

@@ -1,907 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test'
import { z } from 'zod'
import { zodToJsonSchema } from 'zod-to-json-schema'
import { Agent } from '../../src/agent'
import {
ActionError,
ConnectionError,
ExtractionError,
NavigationError,
VerificationError,
} from '../../src/errors'
import type { UIMessageStreamEvent } from '../../src/types'
const TEST_URL = 'http://localhost:9222'
function mockFetch(response: unknown, status = 200) {
return mock(() =>
Promise.resolve({
ok: status >= 200 && status < 300,
status,
json: () => Promise.resolve(response),
} as Response),
)
}
function mockFetchError(error: Error) {
return mock(() => Promise.reject(error))
}
function mockSSEFetch(events: UIMessageStreamEvent[], status = 200) {
const sseData = events.map((e) => `data: ${JSON.stringify(e)}\n\n`).join('')
const encoder = new TextEncoder()
const encoded = encoder.encode(sseData)
return mock(() =>
Promise.resolve({
ok: status >= 200 && status < 300,
status,
body: {
getReader: () => {
let read = false
return {
read: async () => {
if (read) return { done: true, value: undefined }
read = true
return { done: false, value: encoded }
},
releaseLock: () => {},
}
},
},
} as unknown as Response),
)
}
describe('Agent', () => {
let originalFetch: typeof globalThis.fetch
beforeEach(() => {
originalFetch = globalThis.fetch
})
afterEach(() => {
globalThis.fetch = originalFetch
})
describe('constructor', () => {
it('creates agent with url', () => {
const agent = new Agent({ url: TEST_URL })
expect(agent).toBeDefined()
})
it('creates agent with url and llm config', () => {
const agent = new Agent({
url: TEST_URL,
llm: { provider: 'openai', model: 'gpt-4o', apiKey: 'sk-test' },
})
expect(agent).toBeDefined()
})
it('strips trailing slash from url', () => {
const fetchMock = mockFetch({ success: true })
globalThis.fetch = fetchMock
const agent = new Agent({ url: 'http://localhost:9222/' })
agent.nav('https://example.com')
expect(fetchMock).toHaveBeenCalledWith(
'http://localhost:9222/sdk/nav',
expect.any(Object),
)
})
it('generates sessionId when stateful mode is enabled', () => {
const agent = new Agent({ url: TEST_URL, stateful: true })
expect(agent.sessionId).not.toBeNull()
})
it('does not generate sessionId when stateful mode is disabled', () => {
const agent = new Agent({ url: TEST_URL, stateful: false })
expect(agent.sessionId).toBeNull()
})
})
describe('nav()', () => {
it('sends correct request to /sdk/nav', async () => {
const fetchMock = mockFetch({ success: true })
globalThis.fetch = fetchMock
const agent = new Agent({ url: TEST_URL })
await agent.nav('https://example.com')
expect(fetchMock).toHaveBeenCalledWith(
'http://localhost:9222/sdk/nav',
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
}),
)
const call = fetchMock.mock.calls[0]
const body = JSON.parse(call[1].body)
expect(body.url).toBe('https://example.com')
})
it('includes tabId and windowId options', async () => {
const fetchMock = mockFetch({ success: true })
globalThis.fetch = fetchMock
const agent = new Agent({ url: TEST_URL })
await agent.nav('https://example.com', { tabId: 123, windowId: 456 })
const call = fetchMock.mock.calls[0]
const body = JSON.parse(call[1].body)
expect(body.url).toBe('https://example.com')
expect(body.tabId).toBe(123)
expect(body.windowId).toBe(456)
})
it('returns NavResult on success', async () => {
globalThis.fetch = mockFetch({ success: true })
const agent = new Agent({ url: TEST_URL })
const result = await agent.nav('https://example.com')
expect(result).toEqual({ success: true })
})
it('throws NavigationError on failure', async () => {
globalThis.fetch = mockFetch(
{ error: { message: 'Navigation failed' } },
500,
)
const agent = new Agent({ url: TEST_URL })
await expect(agent.nav('https://example.com')).rejects.toThrow(
NavigationError,
)
})
it('throws ConnectionError when fetch fails', async () => {
globalThis.fetch = mockFetchError(new Error('Network error'))
const agent = new Agent({ url: TEST_URL })
await expect(agent.nav('https://example.com')).rejects.toThrow(
ConnectionError,
)
})
it('emits UIMessageStreamEvents', async () => {
globalThis.fetch = mockFetch({ success: true })
const events: UIMessageStreamEvent[] = []
const agent = new Agent({
url: TEST_URL,
onProgress: (e) => events.push(e),
})
await agent.nav('https://example.com')
expect(events.length).toBeGreaterThan(0)
expect(events[0]).toEqual({ type: 'start-step' })
expect(events[events.length - 1]).toEqual({ type: 'finish-step' })
})
})
describe('act()', () => {
it('sends correct request to /sdk/act', async () => {
const fetchMock = mockSSEFetch([
{ type: 'start-step' },
{ type: 'finish-step' },
])
globalThis.fetch = fetchMock
const agent = new Agent({ url: TEST_URL })
await agent.act('click the button')
expect(fetchMock).toHaveBeenCalledWith(
'http://localhost:9222/sdk/act',
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
}),
)
const call = fetchMock.mock.calls[0]
const body = JSON.parse(call[1].body)
expect(body.instruction).toBe('click the button')
expect(body.sessionId).toBeDefined()
})
it('includes context and maxSteps options', async () => {
const fetchMock = mockSSEFetch([
{ type: 'start-step' },
{ type: 'finish-step' },
])
globalThis.fetch = fetchMock
const agent = new Agent({ url: TEST_URL })
await agent.act('search for item', {
context: { query: 'headphones' },
maxSteps: 5,
})
const call = fetchMock.mock.calls[0]
const body = JSON.parse(call[1].body)
expect(body.instruction).toBe('search for item')
expect(body.context).toEqual({ query: 'headphones' })
expect(body.maxSteps).toBe(5)
})
it('includes llm config from constructor', async () => {
const fetchMock = mockSSEFetch([
{ type: 'start-step' },
{ type: 'finish-step' },
])
globalThis.fetch = fetchMock
const llmConfig = {
provider: 'openai' as const,
model: 'gpt-4o',
apiKey: 'sk-test',
}
const agent = new Agent({ url: TEST_URL, llm: llmConfig })
await agent.act('click the button')
const call = fetchMock.mock.calls[0]
const body = JSON.parse(call[1].body)
expect(body.llm).toEqual(llmConfig)
})
it('returns ActResult with steps from SSE stream', async () => {
const fetchMock = mockSSEFetch([
{ type: 'start-step' },
{
type: 'text-delta',
delta: 'I need to click the button',
id: 'thought',
},
{
type: 'tool-input-available',
toolCallId: '1',
toolName: 'browser_click',
input: { nodeId: 1 },
},
{
type: 'tool-output-available',
toolCallId: '1',
output: { success: true },
},
{ type: 'finish-step' },
])
globalThis.fetch = fetchMock
const agent = new Agent({ url: TEST_URL })
const result = await agent.act('click the button')
expect(result.success).toBe(true)
expect(result.steps).toHaveLength(1)
expect(result.steps[0].thought).toBe('I need to click the button')
expect(result.steps[0].toolCalls).toHaveLength(1)
expect(result.steps[0].toolCalls?.[0].name).toBe('browser_click')
})
it('throws ActionError on failure', async () => {
globalThis.fetch = mockFetch({ error: { message: 'Action failed' } }, 500)
const agent = new Agent({ url: TEST_URL })
await expect(agent.act('click the button')).rejects.toThrow(ActionError)
})
it('emits SSE events via onProgress', async () => {
const fetchMock = mockSSEFetch([
{ type: 'start-step' },
{ type: 'text-delta', delta: 'thinking...', id: 'thought' },
{ type: 'finish-step' },
])
globalThis.fetch = fetchMock
const events: UIMessageStreamEvent[] = []
const agent = new Agent({
url: TEST_URL,
onProgress: (e) => events.push(e),
})
await agent.act('click the button')
expect(events).toHaveLength(3)
expect(events[0]).toEqual({ type: 'start-step' })
expect(events[1]).toEqual({
type: 'text-delta',
delta: 'thinking...',
id: 'thought',
})
expect(events[2]).toEqual({ type: 'finish-step' })
})
it('resets sessionId when resetState is true', async () => {
const fetchMock = mockSSEFetch([
{ type: 'start-step' },
{ type: 'finish-step' },
])
globalThis.fetch = fetchMock
const agent = new Agent({ url: TEST_URL })
const originalSessionId = agent.sessionId
await agent.act('click the button', { resetState: true })
expect(agent.sessionId).not.toBe(originalSessionId)
})
it('uses the active tab established by nav() for subsequent act() calls', async () => {
let callCount = 0
globalThis.fetch = mock((url: string, init?: RequestInit) => {
callCount++
if (callCount === 1) {
return Promise.resolve({
ok: true,
status: 200,
json: () =>
Promise.resolve({
success: true,
tabId: 123,
windowId: 456,
}),
} as Response)
}
const sseData = [{ type: 'start-step' }, { type: 'finish-step' }]
.map((event) => `data: ${JSON.stringify(event)}\n\n`)
.join('')
const encoded = new TextEncoder().encode(sseData)
expect(url).toBe('http://localhost:9222/sdk/act')
const body = JSON.parse(init?.body as string)
expect(body.browserContext).toEqual({
windowId: 456,
activeTab: {
id: 123,
url: 'https://example.com',
},
})
return Promise.resolve({
ok: true,
status: 200,
body: {
getReader: () => {
let read = false
return {
read: async () => {
if (read) return { done: true, value: undefined }
read = true
return { done: false, value: encoded }
},
releaseLock: () => {},
}
},
},
} as unknown as Response)
})
const agent = new Agent({ url: TEST_URL })
await agent.nav('https://example.com')
await agent.act('click the button')
})
it('allows act() to override windowId while preserving active tab context', async () => {
const fetchMock = mockSSEFetch([
{ type: 'start-step' },
{ type: 'finish-step' },
])
globalThis.fetch = fetchMock
const agent = new Agent({
url: TEST_URL,
browserContext: {
windowId: 456,
activeTab: {
id: 123,
url: 'https://example.com',
},
},
})
await agent.act('click the button', { windowId: 789 })
const call = fetchMock.mock.calls[0]
const body = JSON.parse(call[1].body)
expect(body.browserContext).toEqual({
windowId: 789,
activeTab: {
id: 123,
url: 'https://example.com',
},
})
})
})
describe('act() with verify option', () => {
it('verifies after action succeeds', async () => {
let callCount = 0
globalThis.fetch = mock(() => {
callCount++
if (callCount === 1) {
// act() SSE response
const encoder = new TextEncoder()
const sseData = [{ type: 'start-step' }, { type: 'finish-step' }]
.map((e) => `data: ${JSON.stringify(e)}\n\n`)
.join('')
return Promise.resolve({
ok: true,
status: 200,
body: {
getReader: () => {
let read = false
return {
read: async () => {
if (read) return { done: true, value: undefined }
read = true
return { done: false, value: encoder.encode(sseData) }
},
releaseLock: () => {},
}
},
},
} as unknown as Response)
}
// verify() JSON response
return Promise.resolve({
ok: true,
status: 200,
json: () => Promise.resolve({ success: true, reason: 'Verified' }),
} as Response)
})
const agent = new Agent({ url: TEST_URL })
const result = await agent.act('click add to cart', {
verify: 'Cart shows 1 item',
})
expect(result.success).toBe(true)
expect(callCount).toBe(2)
})
it('retries when verification fails', async () => {
let callCount = 0
globalThis.fetch = mock(() => {
callCount++
if (callCount === 1 || callCount === 3) {
// act() SSE response
const encoder = new TextEncoder()
const sseData = [{ type: 'start-step' }, { type: 'finish-step' }]
.map((e) => `data: ${JSON.stringify(e)}\n\n`)
.join('')
return Promise.resolve({
ok: true,
status: 200,
body: {
getReader: () => {
let read = false
return {
read: async () => {
if (read) return { done: true, value: undefined }
read = true
return { done: false, value: encoder.encode(sseData) }
},
releaseLock: () => {},
}
},
},
} as unknown as Response)
}
if (callCount === 2) {
// First verify() fails
return Promise.resolve({
ok: true,
status: 200,
json: () =>
Promise.resolve({ success: false, reason: 'Cart is empty' }),
} as Response)
}
// Second verify() succeeds
return Promise.resolve({
ok: true,
status: 200,
json: () =>
Promise.resolve({ success: true, reason: 'Cart has item' }),
} as Response)
})
const agent = new Agent({ url: TEST_URL })
const result = await agent.act('click add to cart', {
verify: 'Cart shows 1 item',
maxRetries: 1,
})
expect(result.success).toBe(true)
expect(callCount).toBe(4) // act, verify(fail), act, verify(pass)
})
it('returns failure when all retries exhausted', async () => {
let callCount = 0
globalThis.fetch = mock(() => {
callCount++
if (callCount % 2 === 1) {
// act() SSE response
const encoder = new TextEncoder()
const sseData = [{ type: 'start-step' }, { type: 'finish-step' }]
.map((e) => `data: ${JSON.stringify(e)}\n\n`)
.join('')
return Promise.resolve({
ok: true,
status: 200,
body: {
getReader: () => {
let read = false
return {
read: async () => {
if (read) return { done: true, value: undefined }
read = true
return { done: false, value: encoder.encode(sseData) }
},
releaseLock: () => {},
}
},
},
} as unknown as Response)
}
// verify() always fails
return Promise.resolve({
ok: true,
status: 200,
json: () =>
Promise.resolve({ success: false, reason: 'Cart is empty' }),
} as Response)
})
const agent = new Agent({ url: TEST_URL })
const result = await agent.act('click add to cart', {
verify: 'Cart shows 1 item',
maxRetries: 2,
})
expect(result.success).toBe(false)
expect(callCount).toBe(6) // (act + verify) x 3
})
})
describe('extract()', () => {
const productSchema = z.object({
name: z.string(),
price: z.number(),
})
it('sends correct request with JSON Schema to /sdk/extract', async () => {
const fetchMock = mockFetch({ data: { name: 'Test', price: 99 } })
globalThis.fetch = fetchMock
const agent = new Agent({ url: TEST_URL })
await agent.extract('get product info', { schema: productSchema })
const expectedJsonSchema = zodToJsonSchema(productSchema)
expect(fetchMock).toHaveBeenCalledWith(
'http://localhost:9222/sdk/extract',
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
}),
)
const call = fetchMock.mock.calls[0]
const body = JSON.parse(call[1].body)
expect(body.instruction).toBe('get product info')
expect(body.schema).toEqual(expectedJsonSchema)
})
it('includes context option', async () => {
const fetchMock = mockFetch({ data: { name: 'Test', price: 99 } })
globalThis.fetch = fetchMock
const agent = new Agent({ url: TEST_URL })
await agent.extract('get product info', {
schema: productSchema,
context: { format: 'USD' },
})
const call = fetchMock.mock.calls[0]
const body = JSON.parse(call[1].body)
expect(body.context).toEqual({ format: 'USD' })
})
it('returns ExtractResult on success', async () => {
const mockData = { name: 'Headphones', price: 99.99 }
globalThis.fetch = mockFetch({ data: mockData })
const agent = new Agent({ url: TEST_URL })
const result = await agent.extract('get product info', {
schema: productSchema,
})
expect(result).toEqual({ data: mockData })
})
it('throws ExtractionError on failure', async () => {
globalThis.fetch = mockFetch(
{ error: { message: 'Extraction failed' } },
422,
)
const agent = new Agent({ url: TEST_URL })
await expect(
agent.extract('get product info', { schema: productSchema }),
).rejects.toThrow(ExtractionError)
})
it('emits UIMessageStreamEvents', async () => {
globalThis.fetch = mockFetch({ data: { name: 'Test', price: 99 } })
const events: UIMessageStreamEvent[] = []
const agent = new Agent({
url: TEST_URL,
onProgress: (e) => events.push(e),
})
await agent.extract('get product info', { schema: productSchema })
expect(events.length).toBeGreaterThan(0)
expect(events[0]).toEqual({ type: 'start-step' })
expect(events[events.length - 1]).toEqual({ type: 'finish-step' })
})
})
describe('verify()', () => {
it('sends correct request to /sdk/verify', async () => {
const fetchMock = mockFetch({ success: true, reason: 'Element visible' })
globalThis.fetch = fetchMock
const agent = new Agent({ url: TEST_URL })
await agent.verify('search results are visible')
expect(fetchMock).toHaveBeenCalledWith(
'http://localhost:9222/sdk/verify',
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
}),
)
const call = fetchMock.mock.calls[0]
const body = JSON.parse(call[1].body)
expect(body.expectation).toBe('search results are visible')
})
it('includes context option', async () => {
const fetchMock = mockFetch({ success: true, reason: 'Element visible' })
globalThis.fetch = fetchMock
const agent = new Agent({ url: TEST_URL })
await agent.verify('price is correct', { context: { expected: 99.99 } })
const call = fetchMock.mock.calls[0]
const body = JSON.parse(call[1].body)
expect(body.context).toEqual({ expected: 99.99 })
})
it('includes llm config from constructor', async () => {
const fetchMock = mockFetch({ success: true, reason: 'Verified' })
globalThis.fetch = fetchMock
const llmConfig = { provider: 'google' as const, model: 'gemini-pro' }
const agent = new Agent({ url: TEST_URL, llm: llmConfig })
await agent.verify('page loaded')
const call = fetchMock.mock.calls[0]
const body = JSON.parse(call[1].body)
expect(body.llm).toEqual(llmConfig)
})
it('includes browser context windowId and active tab id', async () => {
const fetchMock = mockFetch({ success: true, reason: 'Verified' })
globalThis.fetch = fetchMock
const agent = new Agent({
url: TEST_URL,
browserContext: {
windowId: 456,
activeTab: {
id: 123,
url: 'https://example.com',
},
},
})
await agent.verify('the page has some content')
const call = fetchMock.mock.calls[0]
const body = JSON.parse(call[1].body)
expect(body.windowId).toBe(456)
expect(body.tabId).toBe(123)
})
it('returns VerifyResult on success', async () => {
globalThis.fetch = mockFetch({
success: true,
reason: 'Search results found',
})
const agent = new Agent({ url: TEST_URL })
const result = await agent.verify('search results are visible')
expect(result).toEqual({ success: true, reason: 'Search results found' })
})
it('returns VerifyResult with success=false when verification fails', async () => {
globalThis.fetch = mockFetch({
success: false,
reason: 'No search results found',
})
const agent = new Agent({ url: TEST_URL })
const result = await agent.verify('search results are visible')
expect(result).toEqual({
success: false,
reason: 'No search results found',
})
})
it('throws VerificationError on server error', async () => {
globalThis.fetch = mockFetch(
{ error: { message: 'Verification failed' } },
500,
)
const agent = new Agent({ url: TEST_URL })
await expect(agent.verify('search results are visible')).rejects.toThrow(
VerificationError,
)
})
it('emits UIMessageStreamEvents', async () => {
globalThis.fetch = mockFetch({ success: true, reason: 'Verified' })
const events: UIMessageStreamEvent[] = []
const agent = new Agent({
url: TEST_URL,
onProgress: (e) => events.push(e),
})
await agent.verify('page loaded')
expect(events.length).toBeGreaterThan(0)
expect(events[0]).toEqual({ type: 'start-step' })
expect(events[events.length - 1]).toEqual({ type: 'finish-step' })
})
})
describe('onProgress()', () => {
it('allows setting progress callback after construction', async () => {
globalThis.fetch = mockFetch({ success: true })
const events: UIMessageStreamEvent[] = []
const agent = new Agent({ url: TEST_URL })
agent.onProgress((e) => events.push(e))
await agent.nav('https://example.com')
expect(events.length).toBeGreaterThan(0)
expect(events[0]).toEqual({ type: 'start-step' })
})
it('replaces previous callback', async () => {
globalThis.fetch = mockFetch({ success: true })
const events1: UIMessageStreamEvent[] = []
const events2: UIMessageStreamEvent[] = []
const agent = new Agent({
url: TEST_URL,
onProgress: (e) => events1.push(e),
})
agent.onProgress((e) => events2.push(e))
await agent.nav('https://example.com')
expect(events1).toHaveLength(0)
expect(events2.length).toBeGreaterThan(0)
})
})
describe('error handling', () => {
it('includes status code in error', async () => {
globalThis.fetch = mockFetch({ error: { message: 'Not found' } }, 404)
const agent = new Agent({ url: TEST_URL })
try {
await agent.nav('https://example.com')
} catch (error) {
expect(error).toBeInstanceOf(NavigationError)
expect((error as NavigationError).statusCode).toBe(404)
}
})
it('extracts error message from response body', async () => {
globalThis.fetch = mockFetch(
{ error: { message: 'Custom error message' } },
400,
)
const agent = new Agent({ url: TEST_URL })
try {
await agent.nav('https://example.com')
} catch (error) {
expect(error).toBeInstanceOf(NavigationError)
expect((error as NavigationError).message).toBe('Custom error message')
}
})
it('uses default error message when body parse fails', async () => {
globalThis.fetch = mock(() =>
Promise.resolve({
ok: false,
status: 500,
json: () => Promise.reject(new Error('Invalid JSON')),
} as Response),
)
const agent = new Agent({ url: TEST_URL })
try {
await agent.nav('https://example.com')
} catch (error) {
expect(error).toBeInstanceOf(NavigationError)
expect((error as NavigationError).message).toBe(
'Request failed with status 500',
)
}
})
})
describe('dispose()', () => {
it('sends DELETE request to clean up session', async () => {
const fetchMock = mock(() => Promise.resolve({ ok: true } as Response))
globalThis.fetch = fetchMock
const agent = new Agent({ url: TEST_URL })
const sessionId = agent.sessionId
await agent.dispose()
expect(fetchMock).toHaveBeenCalledWith(
`http://localhost:9222/chat/${sessionId}`,
{ method: 'DELETE' },
)
})
it('does not send DELETE when stateful is false', async () => {
const fetchMock = mock(() => Promise.resolve({ ok: true } as Response))
globalThis.fetch = fetchMock
const agent = new Agent({ url: TEST_URL, stateful: false })
await agent.dispose()
expect(fetchMock).not.toHaveBeenCalled()
})
it('only disposes once', async () => {
const fetchMock = mock(() => Promise.resolve({ ok: true } as Response))
globalThis.fetch = fetchMock
const agent = new Agent({ url: TEST_URL })
await agent.dispose()
await agent.dispose()
expect(fetchMock).toHaveBeenCalledTimes(1)
})
})
})

View File

@@ -1,10 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"declaration": true,
"declarationMap": true
},
"include": ["src/**/*"]
}