mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-13 23:53:25 +00:00
Compare commits
1 Commits
fix/patch-
...
fix/clean-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c3b18b834 |
2
.github/workflows/eval-weekly.yml
vendored
2
.github/workflows/eval-weekly.yml
vendored
@@ -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
|
||||
|
||||
165
.github/workflows/release-agent-sdk.yml
vendored
165
.github/workflows/release-agent-sdk.yml
vendored
@@ -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."
|
||||
|
||||
8
.github/workflows/test.yml
vendored
8
.github/workflows/test.yml
vendored
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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?$/
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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=="],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
# @browseros-ai/agent-sdk
|
||||
|
||||
[](https://www.npmjs.com/package/@browseros-ai/agent-sdk)
|
||||
[](../../../../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)
|
||||
@@ -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'],
|
||||
})
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
@@ -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 ?? [],
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"declaration": true,
|
||||
"declarationMap": true
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user