diff --git a/.changeset/README.md b/.changeset/README.md
new file mode 100644
index 0000000..654c6d4
--- /dev/null
+++ b/.changeset/README.md
@@ -0,0 +1,8 @@
+# Changesets
+
+Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
+with multi-package repos, or single-package repos to help you version and publish your code. You can
+find the full documentation for it [in our repository](https://github.com/changesets/changesets).
+
+We have a quick list of common questions to get you started engaging with this project in
+[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md).
diff --git a/.changeset/config.json b/.changeset/config.json
new file mode 100644
index 0000000..ba0f177
--- /dev/null
+++ b/.changeset/config.json
@@ -0,0 +1,11 @@
+{
+ "$schema": "https://unpkg.com/@changesets/config@3.1.3/schema.json",
+ "changelog": "@changesets/cli/changelog",
+ "commit": false,
+ "fixed": [],
+ "linked": [],
+ "access": "public",
+ "baseBranch": "main",
+ "updateInternalDependencies": "patch",
+ "ignore": []
+}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 0faea99..afb5603 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,28 +1,94 @@
name: Release
on:
push:
- tags: ["v*"]
+ branches: [main]
+
+concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
+ pull-requests: write
+ id-token: write
+ outputs:
+ published: ${{ steps.changesets.outputs.published }}
+ publishedPackages: ${{ steps.changesets.outputs.publishedPackages }}
steps:
- uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Check repository hygiene
+ run: |
+ if git ls-files | grep -E '^(docs/plans|\.omx|\.pilot|\.dev-session)(/|$)|(^|/)(AGENTS\.md|CLAUDE\.md|POSITIONING\.md|ROADMAP\.md|BROWSER_USE_PR\.md|demo/RECORD-DEMO-PROMPT\.md|\.staff-engineer-state\.json|\.staff-engineer\.json)$'; then
+ echo "Tracked internal-only files found in public release tree."
+ exit 1
+ fi
+ extra_banners="$(git ls-files .github/banners | grep -Ev '^\.github/banners/05-replay-arrows\.svg$' || true)"
+ if [ -n "$extra_banners" ]; then
+ printf '%s\n' "$extra_banners"
+ echo "Tracked banner drafts found outside the release banner."
+ exit 1
+ fi
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
registry-url: https://registry.npmjs.org
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.12"
- run: npm ci
- - run: npm run build
- - run: npm run typecheck
- - run: npm test
- - run: npm publish --access public
- env:
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- - name: Create GitHub Release
- run: gh release create "${{ github.ref_name }}" --generate-notes
+ - run: npm run release:verify
+ - name: Verify python package
+ run: |
+ python -m pip install -e "./python[dev]" build twine
+ python -m pytest python/tests -q
+ (
+ cd python
+ python -m build
+ python -m twine check dist/*
+ )
+ - name: Verify browser-use integration
+ working-directory: integrations/browser-use
+ run: |
+ npm ci
+ npm run typecheck
+ npm test
+ - name: Verify browserbase integration
+ working-directory: integrations/browserbase
+ run: |
+ npm ci
+ npm test
+ - name: Create release PR or publish npm
+ id: changesets
+ uses: changesets/action@v1
+ with:
+ version: npm run release:version
+ publish: npm run release:publish:npm
env:
- GH_TOKEN: ${{ github.token }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+
+ pypi:
+ runs-on: ubuntu-latest
+ needs: release
+ if: needs.release.outputs.published == 'true'
+ permissions:
+ contents: read
+ id-token: write
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.12"
+ - run: python -m pip install -e "./python[dev]" build
+ - run: python -m pytest python/tests -q
+ - run: python -m build
+ working-directory: python
+ - uses: pypa/gh-action-pypi-publish@release/v1
+ with:
+ packages-dir: python/dist/
diff --git a/.gitignore b/.gitignore
index d98f435..e3edd91 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,19 @@ node_modules/
dist/
.turbo/
*.tsbuildinfo
+.DS_Store
+*.tgz
+.omx/
+.pilot/
.dev-session/
.staff-engineer-state.json
+.staff-engineer.json
+AGENTS.md
+.github/banners/*
+!.github/banners/05-replay-arrows.svg
+docs/plans/
+POSITIONING.md
+ROADMAP.md
+BROWSER_USE_PR.md
+demo/RECORD-DEMO-PROMPT.md
CLAUDE.md
diff --git a/README.md b/README.md
index da94b64..8f83f9f 100644
--- a/README.md
+++ b/README.md
@@ -1,26 +1,77 @@
-
+
+
-**Record a browser session. Replay it. Get the same result.**
+**Replayable proof for production browser agents.**
-Browser automation is inherently non-deterministic — network timing varies, JavaScript timers fire unpredictably, and the same script produces different DOM states across runs. This makes browser-based workflows unreliable to test, impossible to audit, and difficult to trust.
+DBAR turns a browser run into a portable **capsule** you can replay, verify, and keep as a regression artifact.
-DBAR fixes this. It freezes time, records every network response, and captures the full page state at each step. The result is a portable **capsule** — a self-contained artifact you can replay later to verify that the same inputs produce the same outputs.
+If a browser workflow flakes in CI or fails in production, DBAR helps you answer:
+
+- What actually happened?
+- Can I replay it?
+- Where did it diverge first?
+
+DBAR is for teams that need more than logs, screenshots, or trace playback. It captures deterministic time, recorded network, and hashed page state so the run itself becomes an artifact.
+
+## Choose Your Lane
+
+| Lane | Use this when | What you get | Docs |
+|------|---------------|--------------|------|
+| Playwright SDK | DBAR owns the browser session directly | Full deterministic capture, replay, and first-divergence detection | This README |
+| `browser-use` integration | Your workflow already runs in `browser-use` and you need step-level evidence | First-class snapshot, diff, and audit-trail lane for Python/browser-use flows (`browser-use` 0.12.5) | [python/README.md](./python/README.md), [integrations/browser-use/README.md](./integrations/browser-use/README.md) |
+| Browserbase integration | You want DBAR to own a Browserbase-hosted browser session | First-class cloud capture and local replay lane with full deterministic DBAR controls (`@browserbasehq/sdk` 2.9.0) | [integrations/browserbase/README.md](./integrations/browserbase/README.md) |
+
+## Install
+
+For deterministic capture and replay with Playwright:
```bash
npm install @pyyush/dbar playwright-core
```
-## 30-Second Example
+For evidence capsules with `browser-use` on Python:
+
+```bash
+pip install "dbar[browser-use]"
+```
+
+Use the npm package for the full replay engine. Use the PyPI package when your
+workflow already lives in `browser-use` and you want low-friction recording and
+diffing.
+The `browser-use` extra is pinned to `browser-use==0.12.5` and requires
+Python 3.11+.
+
+For Browserbase-hosted deterministic capture and local replay:
+
+```bash
+cd integrations/browserbase
+npm install
+```
+
+## Why Use DBAR
+
+- **Prove what a browser agent did** with a machine-checkable artifact
+- **Reproduce flaky failures** without guessing from logs
+- **Pinpoint the first divergence** instead of diffing a whole run manually
+- **Turn failed runs into regression fixtures** you can keep and replay later
+- **Share one artifact across engineering, support, and audit**
+
+## Integrations
+
+- [browser-use integration](./integrations/browser-use/README.md): official DBAR integration for Python/browser-use workflows. Use it when you need step snapshots, diffs, and a durable audit trail without taking over browser ownership.
+- [Browserbase integration](./integrations/browserbase/README.md): official DBAR integration for Browserbase-managed sessions. Use it when you want full deterministic capture and replay in a cloud browser lane.
+
+## 60-Second Example
```ts
import { chromium } from "playwright-core";
@@ -30,7 +81,6 @@ const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto("https://example.com");
-// Wrap any Playwright workflow — DBAR records everything
const session = await DBAR.capture(page);
await session.step("loaded");
@@ -39,12 +89,9 @@ await session.step("after-click");
const archive = await session.finish();
const capsule = serializeCapsuleArchive(archive);
-// capsule is a portable string — store it, send it, replay it later
```
-That's it. Your existing Playwright code doesn't change. DBAR wraps around it.
-
-## Replay and Verify
+Replay it later:
```ts
import { DBAR, deserializeCapsuleArchive } from "@pyyush/dbar";
@@ -52,37 +99,31 @@ import { DBAR, deserializeCapsuleArchive } from "@pyyush/dbar";
const archive = deserializeCapsuleArchive(capsule);
const result = await DBAR.replay(page, archive);
-result.success // true — every step matched
-result.replaySuccessRate // 1.0
-result.divergences // [] — nothing diverged
+result.success;
+result.replaySuccessRate;
+result.timeToDivergence;
+result.divergences;
```
-If something changed — a new ad loaded, an API returned different data, a timer fired early — DBAR tells you exactly which step diverged and why.
+## Why DBAR Instead of Traces or Session Replay
-## Why This Exists
-
-If you're building any of these, you've hit the non-determinism problem:
-
-- **AI browser agents** — An agent says it filled a form and clicked submit. Did it? Prove it. DBAR gives you a replayable receipt.
-- **Browser test suites** — Your tests pass locally, fail in CI, pass again when you re-run. DBAR captures the exact state so you can diff what changed.
-- **Compliance and audit** — Regulated workflows need evidence. A capsule is a cryptographically-hashed record of exactly what happened in the browser.
-- **Workflow replay** — Record a human performing a task. Replay it programmatically. Verify the replay matches the original.
-
-## How It Works
+Most tools help you **observe** a browser run after the fact.
-DBAR controls three sources of non-determinism at the CDP (Chrome DevTools Protocol) level:
+- Logs show what your code thought it did
+- Screenshots show isolated moments
+- Trace viewers help inspect execution
+- Session replay tools show a recording
-**1. Time** — Virtual time via `Emulation.setVirtualTimePolicy`. `Date.now()`, `setTimeout`, and `requestAnimationFrame` all advance deterministically. No real-world clock jitter.
+DBAR adds **verification**:
-**2. Network** — Every request and response is recorded via the `Fetch` domain. On replay, responses are served from the capsule — same bytes, same order, same timing. Repeated identical requests are matched by `(requestHash, occurrenceIndex)`.
+- Captures the run as a portable capsule
+- Replays under deterministic controls
+- Compares strict observables at each step
+- Reports the first divergence with a durable artifact you can keep
-**3. State** — At each step boundary, DBAR captures the full DOM snapshot, accessibility tree, and screenshot. These are hashed with SHA-256. On replay, the live hashes are compared against the recorded hashes.
+If you need proof, replay, and reusable failure artifacts, DBAR is the right layer.
-The step boundary is yours to define. Call `session.step()` wherever matters — after login, after a click, after data loads. DBAR pauses virtual time, waits for network quiescence, captures everything, then resumes.
-
-## What's in a Capsule
-
-A capsule is a self-contained archive:
+## What Is In A Capsule
```
capsule.json Manifest — environment, seeds, steps, metrics
@@ -92,51 +133,118 @@ snapshots//accessibility.json Accessibility tree
snapshots//screenshot.png Visual screenshot
```
-Everything needed to replay the session is inside. No external dependencies, no database, no API keys. Capsules are validated with Zod schemas and an 8-check integrity suite before replay.
+Everything needed to replay the session is inside the archive.
+
+## How It Works
+
+DBAR controls three sources of nondeterminism at the CDP level:
+
+**1. Time**
+
+Virtual time via `Emulation.setVirtualTimePolicy` makes `Date.now()`, timers, and animation frames deterministic.
-## Strict vs. Advisory Observables
+**2. Network**
+
+Requests and responses are recorded through the `Fetch` domain. On replay, responses are served from the capsule using `(requestHash, occurrenceIndex)` matching.
+
+**3. State**
+
+At each step boundary, DBAR captures the DOM snapshot, accessibility tree, and screenshot. Replay compares the live values against the recorded hashes.
+
+## Strict vs Advisory Observables
| Observable | Strictness | What it proves |
|-----------|-----------|----------------|
| DOM snapshot hash | **Strict** | Page structure is identical |
| Accessibility tree hash | **Strict** | Semantic content is identical |
| Network digest | **Strict** | Same requests got same responses |
-| Screenshot hash | Advisory | Visual appearance (rendering can vary across machines) |
+| Screenshot hash | Advisory | Visual appearance only |
-A replay **passes** when all strict observables match. Screenshot differences are reported but don't fail the replay — pixel-level rendering varies across GPU drivers and OS versions.
+A replay passes when the strict observables match. Screenshot differences are reported, but do not fail the replay.
## Replay Metrics
-Every replay produces three numbers:
+Every replay reports:
+
+- **RSR** — Replay Success Rate
+- **DVR** — Determinism Violation Rate
+- **TTD** — Time to Divergence
+
+Those three numbers let you measure whether a workflow is reproducible and where it stopped being reproducible.
+
+## From Failed Run To Regression Artifact
+
+DBAR should fit the incident loop, not sit beside it.
+
+Capture on failure:
+
+```ts
+import { writeFile } from "node:fs/promises";
+import { DBAR, serializeCapsuleArchive } from "@pyyush/dbar";
+
+const session = await DBAR.capture(page);
+let failed = false;
+
+try {
+ await page.goto("https://example.com/checkout");
+ await session.step("checkout-loaded");
+ await page.click('[data-test=\"submit-order\"]');
+ await session.step("submit-clicked");
+} catch (error) {
+ failed = true;
+ throw error;
+} finally {
+ const archive = await session.finish();
+ if (failed) {
+ await writeFile(
+ "./artifacts/checkout-failure.capsule",
+ serializeCapsuleArchive(archive),
+ "utf8",
+ );
+ }
+}
+```
+
+Replay it later in CI or incident review:
+
+```bash
+npx dbar validate ./artifacts/checkout-failure.capsule
+npx dbar replay ./artifacts/checkout-failure.capsule --json
+```
+
+- `dbar replay` exits with code `1` when a blocking divergence is found
+- `--json` includes `timeToDivergence`, `firstDivergence`, `firstBlockingDivergence`, and the full divergence list
+- screenshot-only mismatches stay advisory, so cosmetic drift does not fail the replay
+
+## Who It Is For
-| Metric | What it means |
-|--------|--------------|
-| **RSR** (Replay Success Rate) | Fraction of steps where all strict observables matched. 1.0 = perfect replay. |
-| **DVR** (Determinism Violation Rate) | `1 - RSR`. 0.0 is what you want. |
-| **TTD** (Time to Divergence) | The first step that diverged. Tells you exactly where things went wrong. |
+- Browser agent teams shipping production workflows
+- Browser automation teams fighting flaky CI and hard-to-reproduce failures
+- Platform and reliability teams that need a standard artifact for browser incidents
+- Audit-sensitive workflows where evidence matters after execution
-## API
+## Core API
### Capture
```ts
const session = await DBAR.capture(page, {
- seeds: { initialTime: 1700000000000 }, // Pin the epoch
- stepBudgetMs: 5000, // Virtual time budget per step
- screenshotMasks: [".ad-banner"], // Mask dynamic content
+ seeds: { initialTime: 1700000000000 },
+ stepBudgetMs: 5000,
+ screenshotMasks: [".ad-banner"],
});
-const snap = await session.step("label"); // Returns StepSnapshot
-const archive = await session.finish(); // Returns CapsuleArchive
-await session.abort(); // Or discard
+const snap = await session.step("label");
+const archive = await session.finish();
+await session.abort();
```
### Replay
```ts
const result = await DBAR.replay(page, archive, {
- unmatchedRequestPolicy: "block", // Block requests not in the transcript
- compareScreenshots: false, // Default — screenshots are advisory
+ unmatchedRequestPolicy: "block",
+ compareScreenshots: false,
});
```
@@ -144,54 +252,57 @@ const result = await DBAR.replay(page, archive, {
```ts
const result = DBAR.validate(archive);
-result.valid // true if capsule is well-formed
-result.checks // 8 individual check results
-result.errors // What's wrong, if anything
+result.valid;
+result.errors;
+result.warnings;
```
### Serialize / Deserialize
```ts
-const blob = serializeCapsuleArchive(archive); // Portable base64 string
-const archive = deserializeCapsuleArchive(blob); // Back to CapsuleArchive
+const blob = serializeCapsuleArchive(archive);
+const archive = deserializeCapsuleArchive(blob);
```
-## Advanced: Lower-Level APIs
+## Lower-Level APIs
-Every subsystem is independently exported for custom integrations:
+Every subsystem is exported independently:
```ts
import {
- // Time control
TimeVirtualizer,
-
- // Network record/replay
NetworkRecorder,
NetworkReplayer,
-
- // Snapshots
captureDOMSnapshot,
captureAccessibilitySnapshot,
captureScreenshot,
-
- // Capsule assembly
buildCapsule,
validateCapsule,
-
- // All Zod schemas for the capsule format
DeterminismCapsuleSchema,
CapsuleStepSchema,
- // ... etc
} from "@pyyush/dbar";
```
-You don't have to use the high-level `DBAR` API. Each piece works standalone with a Playwright `Page` or CDP `CDPSession`.
+Use the high-level `DBAR` API if you want the shortest path. Use the lower-level exports if you need custom integrations.
+
+## Current Product Surface
+
+- **`@pyyush/dbar` on npm**: deterministic capture and replay for Playwright
+- **`dbar` on PyPI**: recorder/diff SDK for `browser-use` flows. See [python/README.md](./python/README.md).
+- **Browserbase integration**: deterministic capture on Browserbase, replay locally. See [integrations/browserbase/README.md](./integrations/browserbase/README.md).
## Requirements
- Node.js >= 20
-- `playwright-core` >= 1.40.0 (peer dependency)
-- Chromium-based browser (CDP is required for virtual time and network interception)
+- `playwright-core` >= 1.40.0
+- Chromium-based browser with CDP support
+
+## More
+
+- [CHANGELOG.md](./CHANGELOG.md) — release notes
+- [python/README.md](./python/README.md) — Python recorder and diff lane
+- [integrations/browser-use/README.md](./integrations/browser-use/README.md) — browser-use integration
+- [integrations/browserbase/README.md](./integrations/browserbase/README.md) — Browserbase integration
## License
diff --git a/demo/dashboard.html b/demo/dashboard.html
new file mode 100644
index 0000000..3303a98
--- /dev/null
+++ b/demo/dashboard.html
@@ -0,0 +1,225 @@
+
+
+
+
+
+DBAR Dashboard
+
+
+
+
+
+
+
+
+
+
Session:
+
waiting...
+
+
+ Site:
+ --
+
+
+
+Steps
+
+
+
+
Capsule
+
+ Requests
+ --
+
+
+ Size
+ --
+
+
+ Steps
+ --
+
+
+
+
+
+
+
Cost Comparison
+
+ browser-use eval
+ --
+
+
+ DBAR replay
+ --
+
+
+ Savings
+ --
+
+
+
+
+
npm install @pyyush/dbar
+
github.com/pyyush/dbar
+
+
+
+
+
+
diff --git a/demo/e2e-test.ts b/demo/e2e-test.ts
new file mode 100644
index 0000000..b0673ee
--- /dev/null
+++ b/demo/e2e-test.ts
@@ -0,0 +1,319 @@
+/**
+ * DBAR End-to-End Test: Capture -> Replay on Real Websites
+ *
+ * Validates that DBAR can capture multi-step browser sessions and replay them
+ * with 100% determinism on static websites.
+ *
+ * Usage: npx tsx demo/e2e-test.ts
+ *
+ * Tested sites:
+ * 1. books.toscrape.com — multi-step: homepage -> category -> book detail
+ * 2. example.com — simple: load page
+ * 3. quotes.toscrape.com — multi-step: homepage -> next page
+ *
+ * @license Apache-2.0
+ */
+
+import { chromium, type Page, type BrowserContext } from "playwright-core";
+
+import { DBAR, type CaptureSession, type ReplaySession } from "../src/sdk.js";
+import type { CapsuleArchive } from "../src/capsule/builder.js";
+import type { ReplayResult } from "../src/capsule/types.js";
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+
+function sleep(ms: number): Promise {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+interface TestResult {
+ site: string;
+ steps: number;
+ captureOk: boolean;
+ replaySuccessRate: number;
+ divergences: string[];
+ error?: string;
+}
+
+// ---------------------------------------------------------------------------
+// Test: books.toscrape.com (3 steps)
+// ---------------------------------------------------------------------------
+
+async function testBooksToscrape(context: BrowserContext): Promise {
+ const site = "books.toscrape.com";
+ const result: TestResult = { site, steps: 0, captureOk: false, replaySuccessRate: 0, divergences: [] };
+
+ let capturePage: Page | undefined;
+ let replayPage: Page | undefined;
+
+ try {
+ // --- Capture ---
+ capturePage = await context.newPage();
+ const session: CaptureSession = await DBAR.capture(capturePage);
+
+ // Step 0: Navigate to homepage
+ await capturePage.goto("https://books.toscrape.com", { waitUntil: "networkidle", timeout: 30000 });
+ await sleep(500);
+ const s0 = await session.step("homepage");
+ console.log(` [capture] step 0 homepage: DOM=${s0.observables.domSnapshotHash.slice(0, 8)}...`);
+
+ // Step 1: Click Travel category
+ await capturePage.click('a[href*="travel"]');
+ await capturePage.waitForLoadState("networkidle");
+ await sleep(500);
+ const s1 = await session.step("category-travel");
+ console.log(` [capture] step 1 category: DOM=${s1.observables.domSnapshotHash.slice(0, 8)}...`);
+
+ // Step 2: Click first book
+ await capturePage.click("article.product_pod h3 a");
+ await capturePage.waitForLoadState("networkidle");
+ await sleep(500);
+ const s2 = await session.step("book-detail");
+ console.log(` [capture] step 2 detail: DOM=${s2.observables.domSnapshotHash.slice(0, 8)}...`);
+
+ const archive: CapsuleArchive = await session.finish();
+ result.steps = archive.manifest.steps.length;
+ result.captureOk = true;
+ console.log(` [capture] done: ${result.steps} steps, ${archive.manifest.metrics.totalNetworkRequests} requests`);
+
+ await capturePage.close();
+ capturePage = undefined;
+
+ // --- Replay ---
+ replayPage = await context.newPage();
+ const rs: ReplaySession = await DBAR.startReplay(replayPage, archive, {
+ unmatchedRequestPolicy: "continue",
+ });
+
+ // Replay step 0: navigate to homepage (same action as capture)
+ await replayPage.goto("https://books.toscrape.com", { waitUntil: "networkidle", timeout: 30000 });
+ await sleep(500);
+ const r0 = await rs.step();
+ console.log(` [replay] step 0 homepage: matched=${r0.matched}`);
+ if (!r0.matched) {
+ result.divergences.push(...r0.divergences.map(d => `step0:${d.type}`));
+ for (const d of r0.divergences) {
+ console.log(` divergence: ${d.type} expected=${d.expected?.slice(0,8)}... actual=${d.actual?.slice(0,8)}...`);
+ }
+ }
+
+ // Replay step 1: click Travel category
+ await replayPage.click('a[href*="travel"]');
+ await replayPage.waitForLoadState("networkidle");
+ await sleep(500);
+ const r1 = await rs.step();
+ console.log(` [replay] step 1 category: matched=${r1.matched}`);
+ if (!r1.matched) result.divergences.push(...r1.divergences.map(d => `step1:${d.type}`));
+
+ // Replay step 2: click first book
+ await replayPage.click("article.product_pod h3 a");
+ await replayPage.waitForLoadState("networkidle");
+ await sleep(500);
+ const r2 = await rs.step();
+ console.log(` [replay] step 2 detail: matched=${r2.matched}`);
+ if (!r2.matched) result.divergences.push(...r2.divergences.map(d => `step2:${d.type}`));
+
+ const replayResult: ReplayResult = await rs.finish();
+ result.replaySuccessRate = replayResult.replaySuccessRate;
+
+ await replayPage.close();
+ replayPage = undefined;
+ } catch (err) {
+ result.error = String(err);
+ } finally {
+ if (capturePage) await capturePage.close().catch(() => {});
+ if (replayPage) await replayPage.close().catch(() => {});
+ }
+
+ return result;
+}
+
+// ---------------------------------------------------------------------------
+// Test: example.com (1 step)
+// ---------------------------------------------------------------------------
+
+async function testExampleCom(context: BrowserContext): Promise {
+ const site = "example.com";
+ const result: TestResult = { site, steps: 0, captureOk: false, replaySuccessRate: 0, divergences: [] };
+
+ let capturePage: Page | undefined;
+ let replayPage: Page | undefined;
+
+ try {
+ // --- Capture ---
+ capturePage = await context.newPage();
+ const session: CaptureSession = await DBAR.capture(capturePage);
+
+ await capturePage.goto("https://example.com", { waitUntil: "networkidle", timeout: 30000 });
+ await sleep(500);
+ const s0 = await session.step("homepage");
+ console.log(` [capture] step 0 homepage: DOM=${s0.observables.domSnapshotHash.slice(0, 8)}...`);
+
+ const archive: CapsuleArchive = await session.finish();
+ result.steps = archive.manifest.steps.length;
+ result.captureOk = true;
+ console.log(` [capture] done: ${result.steps} steps, ${archive.manifest.metrics.totalNetworkRequests} requests`);
+
+ await capturePage.close();
+ capturePage = undefined;
+
+ // --- Replay ---
+ replayPage = await context.newPage();
+ const rs: ReplaySession = await DBAR.startReplay(replayPage, archive, {
+ unmatchedRequestPolicy: "continue",
+ });
+
+ await replayPage.goto("https://example.com", { waitUntil: "networkidle", timeout: 30000 });
+ await sleep(500);
+ const r0 = await rs.step();
+ console.log(` [replay] step 0 homepage: matched=${r0.matched}`);
+ if (!r0.matched) result.divergences.push(...r0.divergences.map(d => `step0:${d.type}`));
+
+ const replayResult: ReplayResult = await rs.finish();
+ result.replaySuccessRate = replayResult.replaySuccessRate;
+
+ await replayPage.close();
+ replayPage = undefined;
+ } catch (err) {
+ result.error = String(err);
+ } finally {
+ if (capturePage) await capturePage.close().catch(() => {});
+ if (replayPage) await replayPage.close().catch(() => {});
+ }
+
+ return result;
+}
+
+// ---------------------------------------------------------------------------
+// Test: quotes.toscrape.com (2 steps)
+// ---------------------------------------------------------------------------
+
+async function testQuotesToscrape(context: BrowserContext): Promise {
+ const site = "quotes.toscrape.com";
+ const result: TestResult = { site, steps: 0, captureOk: false, replaySuccessRate: 0, divergences: [] };
+
+ let capturePage: Page | undefined;
+ let replayPage: Page | undefined;
+
+ try {
+ // --- Capture ---
+ capturePage = await context.newPage();
+ const session: CaptureSession = await DBAR.capture(capturePage);
+
+ await capturePage.goto("https://quotes.toscrape.com", { waitUntil: "networkidle", timeout: 30000 });
+ await sleep(500);
+ const s0 = await session.step("homepage");
+ console.log(` [capture] step 0 homepage: DOM=${s0.observables.domSnapshotHash.slice(0, 8)}...`);
+
+ // Step 1: Click "Next" page link
+ await capturePage.click("li.next a");
+ await capturePage.waitForLoadState("networkidle");
+ await sleep(500);
+ const s1 = await session.step("page-2");
+ console.log(` [capture] step 1 page-2: DOM=${s1.observables.domSnapshotHash.slice(0, 8)}...`);
+
+ const archive: CapsuleArchive = await session.finish();
+ result.steps = archive.manifest.steps.length;
+ result.captureOk = true;
+ console.log(` [capture] done: ${result.steps} steps, ${archive.manifest.metrics.totalNetworkRequests} requests`);
+
+ await capturePage.close();
+ capturePage = undefined;
+
+ // --- Replay ---
+ replayPage = await context.newPage();
+ const rs: ReplaySession = await DBAR.startReplay(replayPage, archive, {
+ unmatchedRequestPolicy: "continue",
+ });
+
+ await replayPage.goto("https://quotes.toscrape.com", { waitUntil: "networkidle", timeout: 30000 });
+ await sleep(500);
+ const r0 = await rs.step();
+ console.log(` [replay] step 0 homepage: matched=${r0.matched}`);
+ if (!r0.matched) result.divergences.push(...r0.divergences.map(d => `step0:${d.type}`));
+
+ await replayPage.click("li.next a");
+ await replayPage.waitForLoadState("networkidle");
+ await sleep(500);
+ const r1 = await rs.step();
+ console.log(` [replay] step 1 page-2: matched=${r1.matched}`);
+ if (!r1.matched) result.divergences.push(...r1.divergences.map(d => `step1:${d.type}`));
+
+ const replayResult: ReplayResult = await rs.finish();
+ result.replaySuccessRate = replayResult.replaySuccessRate;
+
+ await replayPage.close();
+ replayPage = undefined;
+ } catch (err) {
+ result.error = String(err);
+ } finally {
+ if (capturePage) await capturePage.close().catch(() => {});
+ if (replayPage) await replayPage.close().catch(() => {});
+ }
+
+ return result;
+}
+
+// ---------------------------------------------------------------------------
+// Main
+// ---------------------------------------------------------------------------
+
+async function main(): Promise {
+ console.log("DBAR E2E Test — Capture -> Replay on Real Websites");
+ console.log("===================================================\n");
+
+ const browser = await chromium.launch({ headless: true });
+ const context: BrowserContext = await browser.newContext({
+ viewport: { width: 1280, height: 720 },
+ locale: "en-US",
+ timezoneId: "UTC",
+ });
+
+ const results: TestResult[] = [];
+
+ // Run tests sequentially to avoid resource contention
+ for (const [name, testFn] of [
+ ["books.toscrape.com", testBooksToscrape],
+ ["example.com", testExampleCom],
+ ["quotes.toscrape.com", testQuotesToscrape],
+ ] as const) {
+ console.log(`\nTesting ${name}...`);
+ const result = await testFn(context);
+ results.push(result);
+ }
+
+ await browser.close();
+
+ // --- Summary ---
+ console.log("\n===================================================");
+ console.log("RESULTS\n");
+
+ let allPassed = true;
+ for (const r of results) {
+ const status = r.error
+ ? "ERROR"
+ : r.replaySuccessRate === 1
+ ? "PASS"
+ : "FAIL";
+ if (status !== "PASS") allPassed = false;
+
+ console.log(` ${status === "PASS" ? "OK" : "XX"} ${r.site}`);
+ console.log(` steps: ${r.steps}, replay rate: ${(r.replaySuccessRate * 100).toFixed(0)}%`);
+ if (r.divergences.length > 0) {
+ console.log(` divergences: ${r.divergences.join(", ")}`);
+ }
+ if (r.error) {
+ console.log(` error: ${r.error.slice(0, 200)}`);
+ }
+ }
+
+ console.log(`\nOverall: ${allPassed ? "ALL PASSED" : "SOME FAILED"}`);
+ process.exit(allPassed ? 0 : 1);
+}
+
+main().catch((err: unknown) => {
+ console.error("Fatal error:", err);
+ process.exit(1);
+});
diff --git a/demo/record.ts b/demo/record.ts
new file mode 100644
index 0000000..c68d97a
--- /dev/null
+++ b/demo/record.ts
@@ -0,0 +1,572 @@
+/**
+ * DBAR Demo Recording Script
+ *
+ * Produces an Apple-keynote-quality screen recording by orchestrating a headful
+ * Chromium browser alongside a live DBAR dashboard panel. The user starts screen
+ * capture (QuickTime/Loom), runs this script, and gets a polished demo video.
+ *
+ * Usage: npx tsx demo/record.ts
+ *
+ * Layout: Chrome (left 63%) + DBAR Dashboard (right 37%)
+ *
+ * @license Apache-2.0
+ */
+
+import { chromium, type Page, type Browser, type BrowserContext } from "playwright-core";
+import * as path from "node:path";
+import { fileURLToPath } from "node:url";
+
+import { DBAR, type CaptureSession, type ReplaySession } from "../src/sdk.js";
+import type { CapsuleArchive } from "../src/capsule/builder.js";
+import type { StepSnapshot, ReplayResult } from "../src/capsule/types.js";
+
+// ---------------------------------------------------------------------------
+// Constants
+// ---------------------------------------------------------------------------
+
+const SCREEN_WIDTH = 1920;
+const SCREEN_HEIGHT = 1080;
+const MAIN_WIDTH = Math.floor(SCREEN_WIDTH * 0.63);
+const DASH_WIDTH = SCREEN_WIDTH - MAIN_WIDTH;
+const TARGET_URL = "https://books.toscrape.com";
+const DEMO_DIR = path.dirname(fileURLToPath(import.meta.url));
+const DASHBOARD_PATH = path.join(DEMO_DIR, "dashboard.html");
+
+// ---------------------------------------------------------------------------
+// Timing helpers
+// ---------------------------------------------------------------------------
+
+function sleep(ms: number): Promise {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+/** Wait a human-paced delay with slight randomness. */
+function humanDelay(baseMs: number): Promise {
+ return sleep(baseMs + Math.random() * baseMs * 0.3);
+}
+
+// ---------------------------------------------------------------------------
+// Human-like mouse movement (cubic bezier curves)
+// ---------------------------------------------------------------------------
+
+/** Current logical mouse position tracked across moves. */
+let mouseX = MAIN_WIDTH / 2;
+let mouseY = SCREEN_HEIGHT / 2;
+
+/**
+ * Move the mouse along a cubic bezier curve for natural-looking motion.
+ * Control points are randomized to avoid robotic straight lines.
+ */
+async function humanMove(page: Page, x: number, y: number, duration: number = 800): Promise {
+ const startX = mouseX;
+ const startY = mouseY;
+ const cp1x = startX + (x - startX) * 0.3 + (Math.random() - 0.5) * 50;
+ const cp1y = startY + (y - startY) * 0.1 + (Math.random() - 0.5) * 30;
+ const cp2x = startX + (x - startX) * 0.7 + (Math.random() - 0.5) * 50;
+ const cp2y = startY + (y - startY) * 0.9 + (Math.random() - 0.5) * 30;
+
+ const steps = Math.ceil(duration / 16); // ~60fps
+ for (let i = 0; i <= steps; i++) {
+ const t = i / steps;
+ const bx =
+ (1 - t) ** 3 * startX +
+ 3 * (1 - t) ** 2 * t * cp1x +
+ 3 * (1 - t) * t ** 2 * cp2x +
+ t ** 3 * x;
+ const by =
+ (1 - t) ** 3 * startY +
+ 3 * (1 - t) ** 2 * t * cp1y +
+ 3 * (1 - t) * t ** 2 * cp2y +
+ t ** 3 * y;
+ await page.mouse.move(bx, by);
+ await sleep(16);
+ }
+ mouseX = x;
+ mouseY = y;
+}
+
+/**
+ * Click an element with human-like hover-then-click behavior.
+ * Moves to a slightly randomized position within the element bounds,
+ * pauses briefly (human hesitation), then clicks.
+ */
+async function humanClick(page: Page, selector: string): Promise {
+ const el = page.locator(selector);
+ const box = await el.boundingBox();
+ if (!box) {
+ console.warn(`humanClick: element not found for selector "${selector}"`);
+ return;
+ }
+ const x = box.x + box.width * (0.3 + Math.random() * 0.4);
+ const y = box.y + box.height * (0.3 + Math.random() * 0.4);
+ await humanMove(page, x, y);
+ await sleep(200 + Math.random() * 300); // hesitation before clicking
+ await page.mouse.click(x, y);
+}
+
+/**
+ * Smooth scroll by breaking one large wheel event into many small ones.
+ */
+async function humanScroll(page: Page, deltaY: number, steps: number = 10): Promise {
+ for (let i = 0; i < steps; i++) {
+ await page.mouse.wheel(0, deltaY / steps);
+ await sleep(50 + Math.random() * 50);
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Dashboard update helpers
+// ---------------------------------------------------------------------------
+
+/** Set the session status indicator on the dashboard. */
+async function setSessionStatus(
+ dash: Page,
+ status: string,
+ dotClass: string
+): Promise {
+ await dash.evaluate(
+ ([s, cls]) => {
+ const statusEl = document.getElementById("session-status");
+ const dotEl = document.getElementById("session-dot");
+ if (statusEl) statusEl.textContent = s;
+ if (dotEl) {
+ dotEl.className = "session-dot";
+ if (cls) dotEl.classList.add(cls);
+ }
+ },
+ [status, dotClass] as const
+ );
+}
+
+/** Set the site name on the dashboard. */
+async function setSite(dash: Page, site: string): Promise {
+ await dash.evaluate((s) => {
+ const el = document.getElementById("session-site");
+ if (el) el.textContent = s;
+ }, site);
+}
+
+/** Add a step entry to the dashboard step list. */
+async function addStep(
+ dash: Page,
+ index: number,
+ label: string,
+ hash: string
+): Promise {
+ await dash.evaluate(
+ ([idx, lbl, h]) => {
+ const list = document.getElementById("steps-list");
+ if (!list) return;
+ const entry = document.createElement("div");
+ entry.className = "step-entry";
+ entry.innerHTML = `
+ ${idx}
+ ${lbl}
+ ${h.substring(0, 8)}...
+ `;
+ list.appendChild(entry);
+ },
+ [index, label, hash] as const
+ );
+}
+
+/** Update the capsule info box. */
+async function updateCapsule(
+ dash: Page,
+ requests: number,
+ sizeKb: number,
+ steps: number
+): Promise {
+ await dash.evaluate(
+ ([r, s, st]) => {
+ const reqEl = document.getElementById("capsule-requests");
+ const sizeEl = document.getElementById("capsule-size");
+ const stepsEl = document.getElementById("capsule-steps");
+ if (reqEl) reqEl.textContent = r.toLocaleString();
+ if (sizeEl) sizeEl.textContent = `${s.toLocaleString()} KB`;
+ if (stepsEl) stepsEl.textContent = st.toString();
+ },
+ [requests, sizeKb, steps] as const
+ );
+}
+
+/** Add a replay step result to the dashboard. */
+async function addReplayStep(
+ dash: Page,
+ index: number,
+ label: string,
+ isMatch: boolean
+): Promise {
+ await dash.evaluate(
+ ([idx, lbl, match]) => {
+ const section = document.getElementById("replay-section");
+ if (section) {
+ section.classList.add("visible");
+ }
+ const list = document.getElementById("replay-steps-list");
+ if (!list) return;
+ const icon = match ? "MATCH" : "DIVERGED";
+ const cls = match ? "match" : "diverged";
+ const entry = document.createElement("div");
+ entry.className = "step-entry";
+ entry.innerHTML = `
+ ${idx}
+ ${lbl}
+ ${match ? "\u2713" : "\u2717"} ${icon}
+ `;
+ list.appendChild(entry);
+ },
+ [index, label, isMatch] as const
+ );
+}
+
+/** Show the cost comparison box. */
+async function showCost(
+ dash: Page,
+ liveCost: string,
+ replayCost: string,
+ savings: string
+): Promise {
+ await dash.evaluate(
+ ([live, replay, sav]) => {
+ const box = document.getElementById("cost-box");
+ if (box) box.classList.add("visible");
+ const liveEl = document.getElementById("cost-live");
+ const replayEl = document.getElementById("cost-replay");
+ const savEl = document.getElementById("cost-savings");
+ if (liveEl) liveEl.textContent = live;
+ if (replayEl) replayEl.textContent = replay;
+ if (savEl) savEl.textContent = sav;
+ },
+ [liveCost, replayCost, savings] as const
+ );
+}
+
+/** Show the CTA section. */
+async function showCTA(dash: Page): Promise {
+ await dash.evaluate(() => {
+ const cta = document.getElementById("cta");
+ if (cta) cta.classList.add("visible");
+ });
+}
+
+/** Set the phase bar text. */
+async function setPhase(dash: Page, text: string): Promise {
+ await dash.evaluate((t) => {
+ const el = document.getElementById("phase-bar");
+ if (el) el.textContent = t;
+ }, text);
+}
+
+// ---------------------------------------------------------------------------
+// Main recording sequence
+// ---------------------------------------------------------------------------
+
+async function main(): Promise {
+ console.log("DBAR Demo Recorder");
+ console.log("===================");
+ console.log("Start your screen recorder now. The demo begins in 3 seconds...\n");
+ await sleep(3000);
+
+ // Launch headful browser with two windows side by side
+ const browser: Browser = await chromium.launch({
+ headless: false,
+ args: [
+ "--disable-blink-features=AutomationControlled",
+ `--window-position=0,0`,
+ `--window-size=${MAIN_WIDTH},${SCREEN_HEIGHT}`,
+ ],
+ });
+
+ const mainContext: BrowserContext = await browser.newContext({
+ viewport: { width: MAIN_WIDTH - 20, height: SCREEN_HEIGHT - 120 },
+ locale: "en-US",
+ timezoneId: "America/Los_Angeles",
+ });
+
+ const mainPage: Page = await mainContext.newPage();
+
+ // Dashboard window: open in same browser as a separate page
+ // Playwright opens new pages in the same window, so we use a popup approach
+ const dashPage: Page = await mainContext.newPage();
+ await dashPage.goto(`file://${DASHBOARD_PATH}`);
+ await dashPage.setViewportSize({ width: DASH_WIDTH - 20, height: SCREEN_HEIGHT - 120 });
+
+ console.log("Browser launched. Starting demo sequence...\n");
+
+ // ── Scene 1: Navigate (0-15s) ──────────────────────────────────
+ console.log("Scene 1: Navigate to books.toscrape.com");
+ await setPhase(dashPage, "Initializing capture...");
+ await setSessionStatus(dashPage, "starting...", "");
+
+ // Start DBAR capture
+ let session: CaptureSession;
+ try {
+ session = await DBAR.capture(mainPage);
+ } catch (err) {
+ console.error("Failed to start DBAR capture:", err);
+ console.log("Continuing without DBAR capture for demo purposes...");
+ // Fall through with a mock flow if DBAR can't init
+ await runDemoWithoutCapture(mainPage, dashPage, browser);
+ return;
+ }
+
+ await mainPage.goto(TARGET_URL, { waitUntil: "networkidle" });
+ await setSite(dashPage, "books.toscrape.com");
+ await setSessionStatus(dashPage, "active", "active");
+ await setPhase(dashPage, "Capturing...");
+
+ // Take initial step
+ const step0: StepSnapshot = await session.step("homepage");
+ await addStep(dashPage, 0, "homepage", step0.observables.domSnapshotHash);
+ console.log(` Step 0: homepage (DOM: ${step0.observables.domSnapshotHash.substring(0, 8)}...)`);
+
+ await humanDelay(1500);
+
+ // Human-like browsing: scroll down to see book listings
+ await humanMove(mainPage, MAIN_WIDTH / 2, 300);
+ await humanScroll(mainPage, 300, 8);
+ await humanDelay(1000);
+
+ // Hover over a few books
+ const bookLinks = mainPage.locator("article.product_pod h3 a");
+ const bookCount = await bookLinks.count();
+ if (bookCount > 2) {
+ const firstBox = await bookLinks.nth(0).boundingBox();
+ if (firstBox) {
+ await humanMove(mainPage, firstBox.x + firstBox.width / 2, firstBox.y + firstBox.height / 2, 600);
+ await humanDelay(500);
+ }
+ const secondBox = await bookLinks.nth(1).boundingBox();
+ if (secondBox) {
+ await humanMove(mainPage, secondBox.x + secondBox.width / 2, secondBox.y + secondBox.height / 2, 600);
+ await humanDelay(500);
+ }
+ }
+ await humanDelay(1000);
+
+ // ── Scene 2: Browse categories (15-30s) ────────────────────────
+ console.log("Scene 2: Browse Travel category");
+
+ // Scroll back up to see sidebar
+ await humanScroll(mainPage, -200, 6);
+ await humanDelay(800);
+
+ // Click Travel category in sidebar
+ await humanClick(mainPage, 'a[href*="travel"]');
+ await mainPage.waitForLoadState("networkidle");
+ await humanDelay(1000);
+
+ const step1: StepSnapshot = await session.step("category-travel");
+ await addStep(dashPage, 1, "category-travel", step1.observables.domSnapshotHash);
+ console.log(` Step 1: category-travel (DOM: ${step1.observables.domSnapshotHash.substring(0, 8)}...)`);
+
+ await humanScroll(mainPage, 200, 6);
+ await humanDelay(1500);
+
+ // ── Scene 3: View a book (30-45s) ──────────────────────────────
+ console.log("Scene 3: View book detail");
+
+ // Click first book in the listing
+ await humanClick(mainPage, "article.product_pod h3 a");
+ await mainPage.waitForLoadState("networkidle");
+ await humanDelay(1000);
+
+ const step2: StepSnapshot = await session.step("book-detail");
+ await addStep(dashPage, 2, "book-detail", step2.observables.domSnapshotHash);
+ console.log(` Step 2: book-detail (DOM: ${step2.observables.domSnapshotHash.substring(0, 8)}...)`);
+
+ // Slowly scroll through book details
+ await humanScroll(mainPage, 150, 6);
+ await humanDelay(1000);
+ await humanScroll(mainPage, 100, 4);
+ await humanDelay(1500);
+
+ // ── Scene 4: Add to cart (45-55s) ──────────────────────────────
+ console.log("Scene 4: Add to basket");
+
+ // Scroll back up to find the add-to-basket button
+ await humanScroll(mainPage, -200, 6);
+ await humanDelay(500);
+
+ await humanClick(mainPage, "button.btn-primary");
+ await mainPage.waitForLoadState("networkidle");
+ await humanDelay(1000);
+
+ const step3: StepSnapshot = await session.step("add-to-cart");
+ await addStep(dashPage, 3, "add-to-cart", step3.observables.domSnapshotHash);
+ console.log(` Step 3: add-to-cart (DOM: ${step3.observables.domSnapshotHash.substring(0, 8)}...)`);
+
+ await humanDelay(1500);
+
+ // ── Scene 5: Capsule complete (55-65s) ─────────────────────────
+ console.log("Scene 5: Finishing capsule");
+ await setPhase(dashPage, "Finishing capsule...");
+ await setSessionStatus(dashPage, "finishing...", "active");
+ await humanDelay(1500);
+
+ const archive: CapsuleArchive = await session.finish();
+ const capsuleSizeKb = Math.round(archive.manifest.metrics.capsuleSizeBytes / 1024);
+ const totalRequests = archive.manifest.metrics.totalNetworkRequests;
+ const totalSteps = archive.manifest.metrics.totalSteps;
+
+ await updateCapsule(dashPage, totalRequests, capsuleSizeKb, totalSteps);
+ await setSessionStatus(dashPage, "complete", "complete");
+ await setPhase(dashPage, "Capsule saved.");
+
+ console.log(` Capsule: ${totalSteps} steps, ${totalRequests} requests, ${capsuleSizeKb} KB`);
+ await humanDelay(3000);
+
+ // ── Scene 6: Replay & Cost (65-80s) ───────────────────────────
+ console.log("Scene 6: Replay");
+ await setPhase(dashPage, "Replaying capsule...");
+ await setSessionStatus(dashPage, "replaying...", "replaying");
+
+ // Replay in a new tab — use startReplay + same navigation actions as capture
+ const replayPage: Page = await mainContext.newPage();
+
+ try {
+ const rs: ReplaySession = await DBAR.startReplay(replayPage, archive, {
+ unmatchedRequestPolicy: "continue",
+ });
+
+ // Replay step 0: navigate to homepage (same as capture)
+ await replayPage.goto(TARGET_URL, { waitUntil: "networkidle" });
+ await sleep(500);
+ const r0 = await rs.step();
+ await addReplayStep(dashPage, 0, "homepage", r0.matched);
+ await humanDelay(800);
+
+ // Replay step 1: click Travel category
+ await replayPage.click('a[href*="travel"]');
+ await replayPage.waitForLoadState("networkidle");
+ await sleep(500);
+ const r1 = await rs.step();
+ await addReplayStep(dashPage, 1, "category-travel", r1.matched);
+ await humanDelay(800);
+
+ // Replay step 2: click first book
+ await replayPage.click("article.product_pod h3 a");
+ await replayPage.waitForLoadState("networkidle");
+ await sleep(500);
+ const r2 = await rs.step();
+ await addReplayStep(dashPage, 2, "book-detail", r2.matched);
+ await humanDelay(800);
+
+ // Replay step 3: add to cart
+ await replayPage.click("button.btn-primary");
+ await replayPage.waitForLoadState("networkidle");
+ await sleep(500);
+ const r3 = await rs.step();
+ await addReplayStep(dashPage, 3, "add-to-cart", r3.matched);
+ await humanDelay(800);
+
+ const replayResult: ReplayResult = await rs.finish();
+ console.log(` Replay success rate: ${(replayResult.replaySuccessRate * 100).toFixed(0)}%`);
+ } catch (err) {
+ console.error("Replay failed:", err);
+ for (const step of archive.manifest.steps) {
+ const label = step.label ?? `step-${step.index}`;
+ await addReplayStep(dashPage, step.index, label, false);
+ await humanDelay(500);
+ }
+ }
+
+ await replayPage.close();
+ await humanDelay(2000);
+
+ // Show cost comparison
+ await showCost(dashPage, "$0.10", "$0 API", "~99%");
+ await setPhase(dashPage, "Replay complete.");
+ console.log(" Cost: browser-use $0.10/task vs DBAR $0 API cost (~99% savings)");
+ await humanDelay(3000);
+
+ // ── Scene 7: End card (80-90s) ─────────────────────────────────
+ console.log("Scene 7: End card");
+ await showCTA(dashPage);
+ await setPhase(dashPage, "");
+ await humanDelay(5000);
+
+ console.log("\nDemo complete. Closing browser in 5 seconds...");
+ await sleep(5000);
+
+ await browser.close();
+ console.log("Done.");
+}
+
+// ---------------------------------------------------------------------------
+// Fallback: run the visual demo without DBAR capture if SDK init fails
+// (e.g. CDP session issues). This still produces a watchable recording.
+// ---------------------------------------------------------------------------
+
+async function runDemoWithoutCapture(
+ mainPage: Page,
+ dashPage: Page,
+ browser: Browser
+): Promise {
+ console.log("Running visual-only demo (no DBAR capture)...\n");
+
+ await mainPage.goto(TARGET_URL, { waitUntil: "networkidle" });
+ await setSite(dashPage, "books.toscrape.com");
+ await setSessionStatus(dashPage, "active", "active");
+ await setPhase(dashPage, "Capturing (demo mode)...");
+
+ // Simulate steps with placeholder hashes
+ const fakeHash = "a1b2c3d4e5f67890";
+
+ await addStep(dashPage, 0, "homepage", fakeHash);
+ await humanDelay(2000);
+ await humanScroll(mainPage, 300, 8);
+ await humanDelay(1500);
+
+ // Navigate to Travel
+ await humanClick(mainPage, 'a[href*="travel"]');
+ await mainPage.waitForLoadState("networkidle");
+ await addStep(dashPage, 1, "category-travel", fakeHash);
+ await humanDelay(2000);
+
+ // Click first book
+ await humanClick(mainPage, "article.product_pod h3 a");
+ await mainPage.waitForLoadState("networkidle");
+ await addStep(dashPage, 2, "book-detail", fakeHash);
+ await humanDelay(2000);
+
+ // Add to cart
+ await humanClick(mainPage, "button.btn-primary");
+ await mainPage.waitForLoadState("networkidle");
+ await addStep(dashPage, 3, "add-to-cart", fakeHash);
+ await humanDelay(2000);
+
+ await updateCapsule(dashPage, 42, 856, 4);
+ await setSessionStatus(dashPage, "complete", "complete");
+ await humanDelay(2000);
+
+ // Replay simulation
+ await setPhase(dashPage, "Replaying capsule...");
+ for (let i = 0; i < 4; i++) {
+ const labels = ["homepage", "category-travel", "book-detail", "add-to-cart"];
+ await addReplayStep(dashPage, i, labels[i]!, true);
+ await humanDelay(800);
+ }
+
+ await showCost(dashPage, "$0.10", "$0 API", "~99%");
+ await setPhase(dashPage, "Replay complete.");
+ await humanDelay(3000);
+ await showCTA(dashPage);
+ await setPhase(dashPage, "");
+ await humanDelay(5000);
+
+ console.log("\nDemo complete. Closing browser in 5 seconds...");
+ await sleep(5000);
+ await browser.close();
+ console.log("Done.");
+}
+
+// ---------------------------------------------------------------------------
+// Entry point
+// ---------------------------------------------------------------------------
+
+main().catch((err: unknown) => {
+ console.error("Fatal error:", err);
+ process.exit(1);
+});
diff --git a/integrations/browser-use/README.md b/integrations/browser-use/README.md
new file mode 100644
index 0000000..d13bb2c
--- /dev/null
+++ b/integrations/browser-use/README.md
@@ -0,0 +1,207 @@
+# DBAR browser-use Integration
+
+First-class DBAR integration for `browser-use` workflows.
+
+Use this lane when your agent already runs inside `browser-use` and you need
+step-level DOM, accessibility, and screenshot evidence without asking DBAR to
+take over browser ownership.
+
+Compared with the Browserbase integration, this lane is intentionally
+observe-only: it gives you snapshots, diffs, and an audit trail, not full
+deterministic network/time replay.
+
+This integration is verified against these exact versions:
+
+- Python 3.11+
+- `browser-use==0.12.5`
+- `langchain-openai==0.1.25` for `example.py`
+- `playwright-core==1.58.2`
+- `ts-node==10.9.2`
+- `typescript==5.9.3`
+- `vitest==4.1.2`
+
+## What This Does
+
+DBAR connects to the same Chrome instance your browser-use agent is using
+and captures DOM snapshots, accessibility trees, and screenshots at each
+agent step boundary. This gives you a verifiable audit trail of what the
+page looked like at each point in the agent's execution.
+
+Each artifact is hashed with SHA-256 for integrity verification.
+
+## What This Does Not Do
+
+- Does not record network traffic
+- Does not freeze time
+- Does not produce a replayable determinism capsule
+
+This sidecar intentionally stays out of browser-use's control loop. For full
+deterministic capture and replay, use DBAR directly with Playwright or the
+[Browserbase integration](../browserbase/README.md).
+
+## Integration Contract With browser-use 0.12.5
+
+The supported hook surface is:
+
+```python
+await agent.run(on_step_end=...)
+```
+
+The integration should not pass `on_step_end` into `Agent(...)`.
+
+The browser session should be started before the sidecar is launched so you can
+hand DBAR the real `browser.cdp_url` chosen by browser-use:
+
+```python
+browser = Browser(headless=False)
+await browser.start()
+cdp_url = browser.cdp_url
+```
+
+browser-use launches local Chrome on a free remote-debugging port, so assuming
+a fixed `9222` port is incorrect.
+
+## How It Works
+
+At each step:
+
+1. browser-use runs a step and calls `on_step_end(agent)`
+2. The Python hook writes a `.dbar-step` signal file
+3. The signal payload includes both the step label and the current
+ `agent.browser_session.agent_focus_target_id`
+4. The Node.js sidecar resolves the matching page target over CDP
+5. DBAR captures DOM snapshot + accessibility tree + screenshot for that page
+6. On `.dbar-finish`, a manifest JSON is written with all step data
+
+```text
+browser-use (Python) DBAR capture (Node.js)
+ | |
+ +- Browser() +- chromium.connectOverCDP(cdpUrl)
+ +- await browser.start() |
+ | +- resolve page by targetId each step
+ +- agent.run( |
+ | on_step_end=signal_step | <- watches .dbar-step files
+ | ) |
+ | +- DOMSnapshot.captureSnapshot
+ +- on_step_end writes JSON +- Accessibility.getFullAXTree
+ | {label, targetId} +- Page.captureScreenshot
+ +- agent finishes |
+ +- writes .dbar-finish +- writes manifest.json
+ | |
+ +- done +- done
+```
+
+## Pinned Versions
+
+- browser-use: 0.12.5
+- cdp-use: 1.4.5
+- langchain-openai: 0.1.25 for `example.py`
+- playwright-core: 1.58.2
+- ts-node: 10.9.2
+- typescript: 5.9.3
+- vitest: 4.1.2
+
+## Setup
+
+### 1. Install Python dependencies
+
+```bash
+python3.11 -m venv .venv
+source .venv/bin/activate
+pip install -r requirements.txt
+```
+
+### 2. Install Node.js dependencies
+
+```bash
+cd integrations/browser-use
+npm install
+```
+
+### 3. Set API key
+
+```bash
+export OPENAI_API_KEY="sk-..."
+# Or use ChatAnthropic + ANTHROPIC_API_KEY instead
+```
+
+## Usage
+
+### Run the example
+
+```bash
+python example.py
+```
+
+### Run capture sidecar manually
+
+In your Python process:
+
+```python
+browser = Browser(headless=False)
+await browser.start()
+print(browser.cdp_url)
+```
+
+Then start the capture sidecar with that CDP URL:
+
+```bash
+npx tsx capture.ts "$BROWSER_USE_CDP_URL" ./dbar-snapshots
+```
+
+Signal DBAR at step boundaries:
+
+```python
+from pathlib import Path
+import json
+
+Path(".dbar-step").write_text(json.dumps({
+ "label": "after-login",
+ "targetId": agent.browser_session.agent_focus_target_id,
+}))
+```
+
+When the agent is done:
+
+```bash
+touch .dbar-finish
+```
+
+## Output
+
+The sidecar writes to `./dbar-snapshots/`:
+
+```text
+dbar-snapshots/
+ manifest.json
+ step-001/
+ dom.json
+ a11y.json
+ screenshot.png
+ step-002/
+ ...
+```
+
+## File-Based Signaling
+
+| Signal file | Effect |
+|----------------|--------|
+| `.dbar-step` | Captures a step. Accepts either a plain label or JSON `{ "label": "...", "targetId": "..." }`. |
+| `.dbar-finish` | Ends the session and writes the manifest to disk. |
+
+Signal files are consumed after being read. The sidecar polls every 250ms.
+
+## Files
+
+| File | Description |
+|------|-------------|
+| `capture.ts` | Node.js sidecar: CDP attach, target resolution, snapshot loop, manifest |
+| `capture.test.ts` | Unit tests for capture utilities |
+| `example.py` | End-to-end Python example with browser-use |
+| `requirements.txt` | Pinned Python dependencies |
+| `package.json` | Node.js dependencies |
+| `tsconfig.json` | TypeScript configuration |
+
+## License
+
+Apache-2.0
diff --git a/integrations/browser-use/capture.test.ts b/integrations/browser-use/capture.test.ts
new file mode 100644
index 0000000..c27e616
--- /dev/null
+++ b/integrations/browser-use/capture.test.ts
@@ -0,0 +1,260 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import { createHash } from "node:crypto";
+import {
+ parseArgs,
+ cleanSignalFiles,
+ waitForSignal,
+ captureStepSnapshot,
+ buildManifest,
+ type StepRecord,
+} from "./capture.js";
+import * as fs from "node:fs";
+
+vi.mock("node:fs", async () => {
+ const actual = await vi.importActual("node:fs");
+ return {
+ ...actual,
+ existsSync: vi.fn(),
+ readFileSync: vi.fn(),
+ unlinkSync: vi.fn(),
+ mkdirSync: vi.fn(),
+ writeFileSync: vi.fn(),
+ };
+});
+
+describe("parseArgs", () => {
+ const originalArgv = process.argv;
+
+ afterEach(() => {
+ process.argv = originalArgv;
+ });
+
+ it("shouldUseDefaultsWhenNoArgsProvided", () => {
+ // Given no CLI arguments beyond node and script
+ process.argv = ["node", "capture.ts"];
+
+ // When parsing args
+ const args = parseArgs();
+
+ // Then defaults are used
+ expect(args.cdpUrl).toBe("http://localhost:9222");
+ expect(args.outputDir).toContain("dbar-snapshots");
+ });
+
+ it("shouldParseCustomCdpUrlAndOutputDir", () => {
+ // Given custom CLI arguments
+ process.argv = ["node", "capture.ts", "http://localhost:9333", "/tmp/out"];
+
+ // When parsing args
+ const args = parseArgs();
+
+ // Then custom values are used
+ expect(args.cdpUrl).toBe("http://localhost:9333");
+ expect(args.outputDir).toBe("/tmp/out");
+ });
+});
+
+describe("cleanSignalFiles", () => {
+ beforeEach(() => {
+ vi.mocked(fs.existsSync).mockReset();
+ vi.mocked(fs.unlinkSync).mockReset();
+ });
+
+ it("shouldRemoveExistingSignalFiles", () => {
+ // Given both signal files exist
+ vi.mocked(fs.existsSync).mockReturnValue(true);
+
+ // When cleaning
+ cleanSignalFiles();
+
+ // Then both are removed
+ expect(fs.unlinkSync).toHaveBeenCalledWith(".dbar-step");
+ expect(fs.unlinkSync).toHaveBeenCalledWith(".dbar-finish");
+ });
+
+ it("shouldNotThrowWhenSignalFilesDoNotExist", () => {
+ // Given no signal files exist
+ vi.mocked(fs.existsSync).mockReturnValue(false);
+
+ // When cleaning
+ // Then it does not throw
+ expect(() => cleanSignalFiles()).not.toThrow();
+ expect(fs.unlinkSync).not.toHaveBeenCalled();
+ });
+});
+
+describe("waitForSignal", () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ vi.mocked(fs.existsSync).mockReset();
+ vi.mocked(fs.readFileSync).mockReset();
+ vi.mocked(fs.unlinkSync).mockReset();
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ it("shouldResolveWithStepWhenStepSignalAppears", async () => {
+ // Given step signal appears on the second poll
+ let callCount = 0;
+ vi.mocked(fs.existsSync).mockImplementation((path: fs.PathLike) => {
+ const p = String(path);
+ if (p === ".dbar-finish") return false;
+ if (p === ".dbar-step") {
+ callCount++;
+ return callCount >= 2;
+ }
+ return false;
+ });
+ vi.mocked(fs.readFileSync).mockReturnValue("step-1");
+
+ // When waiting for a signal
+ const promise = waitForSignal();
+ await vi.advanceTimersByTimeAsync(600);
+
+ const result = await promise;
+
+ // Then it resolves with step type and the label
+ expect(result).toEqual({ type: "step", label: "step-1" });
+ expect(fs.unlinkSync).toHaveBeenCalledWith(".dbar-step");
+ });
+
+ it("shouldResolveWithFinishWhenFinishSignalAppears", async () => {
+ // Given finish signal appears on the first poll
+ vi.mocked(fs.existsSync).mockImplementation((path: fs.PathLike) => {
+ return String(path) === ".dbar-finish";
+ });
+
+ // When waiting
+ const promise = waitForSignal();
+ await vi.advanceTimersByTimeAsync(300);
+
+ const result = await promise;
+
+ // Then it resolves with finish type
+ expect(result).toEqual({ type: "finish" });
+ });
+
+ it("shouldUseDefaultLabelWhenStepFileIsEmpty", async () => {
+ // Given step signal with empty content
+ vi.mocked(fs.existsSync).mockImplementation((path: fs.PathLike) => {
+ if (String(path) === ".dbar-finish") return false;
+ return String(path) === ".dbar-step";
+ });
+ vi.mocked(fs.readFileSync).mockReturnValue(" ");
+
+ // When waiting
+ const promise = waitForSignal();
+ await vi.advanceTimersByTimeAsync(300);
+
+ const result = await promise;
+
+ // Then label defaults to "step"
+ expect(result).toEqual({ type: "step", label: "step" });
+ });
+});
+
+describe("captureStepSnapshot", () => {
+ it("shouldCaptureDomA11yAndScreenshotWithHashes", async () => {
+ // Given mock CDP session and Page
+ const domData = { documents: [], strings: [] };
+ const mockCdp = {
+ send: vi.fn().mockImplementation((method: string) => {
+ if (method === "DOMSnapshot.enable") return Promise.resolve();
+ if (method === "DOMSnapshot.captureSnapshot") return Promise.resolve(domData);
+ if (method === "Accessibility.getFullAXTree") {
+ return Promise.resolve({ nodes: [{ role: { value: "WebArea" } }] });
+ }
+ if (method === "Page.captureScreenshot") {
+ return Promise.resolve({ data: Buffer.from("fake-png").toString("base64") });
+ }
+ return Promise.resolve();
+ }),
+ };
+
+ // When capturing a step snapshot
+ const record = await captureStepSnapshot(mockCdp as any, "step-1", 1);
+
+ // Then it returns a StepRecord with all three snapshots hashed
+ expect(record.label).toBe("step-1");
+ expect(record.stepNumber).toBe(1);
+ expect(record.domHash).toMatch(/^[a-f0-9]{64}$/);
+ expect(record.a11yHash).toMatch(/^[a-f0-9]{64}$/);
+ expect(record.screenshotHash).toMatch(/^[a-f0-9]{64}$/);
+ expect(record.timestamp).toBeDefined();
+ });
+
+ it("shouldProduceDeterministicHashesForSameInput", async () => {
+ // Given two identical CDP responses
+ const domData = { documents: [{ nodes: [1] }], strings: ["a"] };
+ const a11yData = { nodes: [{ role: { value: "button" } }] };
+ const screenshotBase64 = Buffer.from("same-png").toString("base64");
+
+ const makeCdp = () => ({
+ send: vi.fn().mockImplementation((method: string) => {
+ if (method === "DOMSnapshot.enable") return Promise.resolve();
+ if (method === "DOMSnapshot.captureSnapshot") return Promise.resolve(domData);
+ if (method === "Accessibility.getFullAXTree") return Promise.resolve(a11yData);
+ if (method === "Page.captureScreenshot") return Promise.resolve({ data: screenshotBase64 });
+ return Promise.resolve();
+ }),
+ });
+
+ // When capturing twice
+ const r1 = await captureStepSnapshot(makeCdp() as any, "s1", 1);
+ const r2 = await captureStepSnapshot(makeCdp() as any, "s1", 1);
+
+ // Then hashes are identical
+ expect(r1.domHash).toBe(r2.domHash);
+ expect(r1.a11yHash).toBe(r2.a11yHash);
+ expect(r1.screenshotHash).toBe(r2.screenshotHash);
+ });
+});
+
+describe("buildManifest", () => {
+ it("shouldBuildManifestWithAllSteps", () => {
+ // Given step records
+ const steps: StepRecord[] = [
+ {
+ label: "step-1",
+ stepNumber: 1,
+ timestamp: "2026-03-26T00:00:00.000Z",
+ domHash: "aaa",
+ a11yHash: "bbb",
+ screenshotHash: "ccc",
+ },
+ {
+ label: "step-2",
+ stepNumber: 2,
+ timestamp: "2026-03-26T00:00:01.000Z",
+ domHash: "ddd",
+ a11yHash: "eee",
+ screenshotHash: "fff",
+ },
+ ];
+
+ // When building manifest
+ const manifest = buildManifest(steps, "http://localhost:9222");
+
+ // Then it contains all steps and metadata
+ expect(manifest.version).toBe("1.0.0");
+ expect(manifest.cdpUrl).toBe("http://localhost:9222");
+ expect(manifest.steps).toHaveLength(2);
+ expect(manifest.steps[0]!.label).toBe("step-1");
+ expect(manifest.steps[1]!.domHash).toBe("ddd");
+ expect(manifest.captureMode).toBe("snapshot-only");
+ expect(manifest.createdAt).toBeDefined();
+ });
+
+ it("shouldSetCaptureModeLimitations", () => {
+ // Given empty steps
+ const manifest = buildManifest([], "http://localhost:9222");
+
+ // Then manifest documents limitations
+ expect(manifest.captureMode).toBe("snapshot-only");
+ expect(manifest.limitations).toContain("no-network-recording");
+ expect(manifest.limitations).toContain("no-virtual-time");
+ expect(manifest.limitations).toContain("no-deterministic-replay");
+ });
+});
diff --git a/integrations/browser-use/capture.ts b/integrations/browser-use/capture.ts
new file mode 100644
index 0000000..cfa6aee
--- /dev/null
+++ b/integrations/browser-use/capture.ts
@@ -0,0 +1,331 @@
+/**
+ * DBAR Snapshot Capture Sidecar for browser-use
+ *
+ * Connects to a running Chrome instance via CDP and captures page state
+ * snapshots (DOM, accessibility tree, screenshot) at step boundaries
+ * signaled by browser-use's on_step_end hook via the filesystem.
+ *
+ * This is a "snapshot-only" capture mode: it does NOT enable virtual time
+ * or network interception, which would conflict with browser-use's own
+ * CDP usage via cdp-use. For full deterministic capture and replay,
+ * use DBAR directly with Playwright.
+ *
+ * Usage:
+ * npx tsx capture.ts [cdpUrl] [outputDir]
+ *
+ * Defaults:
+ * cdpUrl = http://localhost:9222
+ * outputDir = ./dbar-snapshots
+ *
+ * @module
+ */
+
+import { createHash } from "node:crypto";
+import { existsSync, readFileSync, unlinkSync, mkdirSync, writeFileSync } from "node:fs";
+import { join, resolve } from "node:path";
+
+const STEP_SIGNAL = ".dbar-step";
+const FINISH_SIGNAL = ".dbar-finish";
+const POLL_INTERVAL_MS = 250;
+
+/** A record of captured snapshot data for a single step. */
+export interface StepRecord {
+ label: string;
+ stepNumber: number;
+ timestamp: string;
+ domHash: string;
+ a11yHash: string;
+ screenshotHash: string;
+}
+
+/** Manifest written at the end of a capture session. */
+export interface CaptureManifest {
+ version: string;
+ cdpUrl: string;
+ captureMode: string;
+ limitations: string[];
+ createdAt: string;
+ steps: StepRecord[];
+}
+
+/**
+ * Parse CLI arguments for the capture sidecar.
+ *
+ * @returns cdpUrl and outputDir parsed from process.argv, with defaults.
+ */
+export function parseArgs(): { cdpUrl: string; outputDir: string } {
+ const cdpUrl = process.argv[2] ?? "http://localhost:9222";
+ const outputDir = resolve(process.argv[3] ?? "./dbar-snapshots");
+ return { cdpUrl, outputDir };
+}
+
+/**
+ * Remove stale signal files from a previous run.
+ * Safe to call when files do not exist.
+ */
+export function cleanSignalFiles(): void {
+ for (const signal of [STEP_SIGNAL, FINISH_SIGNAL]) {
+ if (existsSync(signal)) {
+ unlinkSync(signal);
+ }
+ }
+}
+
+/**
+ * Poll the filesystem for step or finish signal files.
+ *
+ * @returns A promise that resolves when a signal is detected.
+ * - `{ type: "step", label: string }` when `.dbar-step` appears
+ * - `{ type: "finish" }` when `.dbar-finish` appears
+ */
+export function waitForSignal(): Promise<{ type: "step"; label: string } | { type: "finish" }> {
+ return new Promise((resolve) => {
+ const interval = setInterval(() => {
+ if (existsSync(FINISH_SIGNAL)) {
+ clearInterval(interval);
+ try { unlinkSync(FINISH_SIGNAL); } catch { /* already removed */ }
+ resolve({ type: "finish" });
+ return;
+ }
+
+ if (existsSync(STEP_SIGNAL)) {
+ const raw = readFileSync(STEP_SIGNAL, "utf-8").trim();
+ const label = raw || "step";
+ try { unlinkSync(STEP_SIGNAL); } catch { /* already removed */ }
+ clearInterval(interval);
+ resolve({ type: "step", label });
+ }
+ }, POLL_INTERVAL_MS);
+ });
+}
+
+/**
+ * Canonicalize a value to a deterministic JSON string with sorted keys.
+ * Arrays preserve element order; only object key order is normalized.
+ */
+function canonicalize(value: unknown): string {
+ return JSON.stringify(value, (_key, val: unknown) => {
+ if (val && typeof val === "object" && !Array.isArray(val)) {
+ const sorted: Record = {};
+ for (const k of Object.keys(val).sort()) {
+ sorted[k] = (val as Record)[k];
+ }
+ return sorted;
+ }
+ return val;
+ });
+}
+
+/**
+ * Capture DOM snapshot, accessibility tree, and screenshot at a step boundary
+ * using raw CDP commands. Does not use DBAR's high-level capture API to avoid
+ * enabling virtual time or Fetch interception.
+ *
+ * @param cdpSession - A CDP session (e.g., from Playwright's `page.createCDPSession()`)
+ * @param label - Human-readable label for this step
+ * @param stepNumber - Sequential step number
+ * @returns A StepRecord with SHA-256 hashes for each artifact
+ */
+export async function captureStepSnapshot(
+ cdpSession: { send: (method: string, params?: Record) => Promise },
+ label: string,
+ stepNumber: number,
+): Promise {
+ // DOM snapshot via CDP
+ await cdpSession.send("DOMSnapshot.enable");
+ const domSnapshot = await cdpSession.send("DOMSnapshot.captureSnapshot", {
+ computedStyles: ["display", "visibility", "opacity", "position"],
+ includePaintOrder: false,
+ includeDOMRects: true,
+ });
+ const domSerialized = canonicalize(domSnapshot);
+ const domHash = createHash("sha256").update(domSerialized).digest("hex");
+
+ // Accessibility tree via CDP (not Playwright's page.accessibility)
+ const a11yTree = await cdpSession.send("Accessibility.getFullAXTree");
+ const a11ySerialized = canonicalize(a11yTree);
+ const a11yHash = createHash("sha256").update(a11ySerialized).digest("hex");
+
+ // Screenshot via CDP Page.captureScreenshot
+ const screenshotResult = await cdpSession.send("Page.captureScreenshot", {
+ format: "png",
+ }) as { data: string };
+ const screenshotBuffer = Buffer.from(screenshotResult.data, "base64");
+ const screenshotHash = createHash("sha256").update(screenshotBuffer).digest("hex");
+
+ return {
+ label,
+ stepNumber,
+ timestamp: new Date().toISOString(),
+ domHash,
+ a11yHash,
+ screenshotHash,
+ };
+}
+
+/**
+ * Build a capture manifest from collected step records.
+ *
+ * @param steps - All captured step records
+ * @param cdpUrl - The CDP URL used for the session
+ * @returns A CaptureManifest documenting the session
+ */
+export function buildManifest(steps: StepRecord[], cdpUrl: string): CaptureManifest {
+ return {
+ version: "1.0.0",
+ cdpUrl,
+ captureMode: "snapshot-only",
+ limitations: [
+ "no-network-recording",
+ "no-virtual-time",
+ "no-deterministic-replay",
+ ],
+ createdAt: new Date().toISOString(),
+ steps,
+ };
+}
+
+/**
+ * Write step artifacts (DOM JSON, a11y JSON, screenshot PNG) and manifest
+ * to the output directory.
+ */
+function writeArtifacts(
+ outputDir: string,
+ steps: StepRecord[],
+ artifacts: Map,
+ cdpUrl: string,
+): string {
+ mkdirSync(outputDir, { recursive: true });
+
+ for (const [stepNum, data] of artifacts) {
+ const stepDir = join(outputDir, `step-${String(stepNum).padStart(3, "0")}`);
+ mkdirSync(stepDir, { recursive: true });
+ writeFileSync(join(stepDir, "dom.json"), data.dom, "utf-8");
+ writeFileSync(join(stepDir, "a11y.json"), data.a11y, "utf-8");
+ writeFileSync(join(stepDir, "screenshot.png"), data.screenshot);
+ }
+
+ const manifest = buildManifest(steps, cdpUrl);
+ const manifestPath = join(outputDir, "manifest.json");
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
+
+ return manifestPath;
+}
+
+// -- CLI entrypoint --
+// Only runs when executed directly, not when imported for testing.
+
+const isMainModule = process.argv[1]?.endsWith("capture.ts") ||
+ process.argv[1]?.endsWith("capture.js");
+
+if (isMainModule) {
+ main().catch((error: unknown) => {
+ console.error("[dbar-capture] Fatal error:", error);
+ process.exit(1);
+ });
+}
+
+async function main(): Promise {
+ // Dynamic import to avoid requiring playwright-core at test time
+ const { chromium } = await import("playwright-core");
+
+ const { cdpUrl, outputDir } = parseArgs();
+
+ console.log(`[dbar-capture] Connecting to browser at ${cdpUrl}`);
+
+ const browser = await chromium.connectOverCDP(cdpUrl);
+ const contexts = browser.contexts();
+
+ if (contexts.length === 0) {
+ console.error("[dbar-capture] No browser contexts found. Is a page open?");
+ process.exit(1);
+ }
+
+ const context = contexts[0]!;
+ const pages = context.pages();
+
+ if (pages.length === 0) {
+ console.error("[dbar-capture] No pages found in the browser context.");
+ process.exit(1);
+ }
+
+ const page = pages[0]!;
+ console.log(`[dbar-capture] Attached to page: ${page.url()}`);
+
+ const cdpSession = await page.context().newCDPSession(page);
+
+ cleanSignalFiles();
+
+ console.log("[dbar-capture] Ready. Waiting for signals...");
+ console.log(`[dbar-capture] Write to '${STEP_SIGNAL}' to capture a step`);
+ console.log(`[dbar-capture] Write to '${FINISH_SIGNAL}' to finish`);
+
+ const steps: StepRecord[] = [];
+ const artifacts = new Map();
+ let stepCount = 0;
+
+ let running = true;
+ while (running) {
+ const signal = await waitForSignal();
+
+ if (signal.type === "step") {
+ stepCount++;
+ console.log(`[dbar-capture] Step signal: "${signal.label}" (#${stepCount})`);
+
+ try {
+ // Capture snapshots via CDP
+ await cdpSession.send("DOMSnapshot.enable" as any);
+ const domSnapshot = await cdpSession.send(
+ "DOMSnapshot.captureSnapshot" as any,
+ {
+ computedStyles: ["display", "visibility", "opacity", "position"],
+ includePaintOrder: false,
+ includeDOMRects: true,
+ } as any,
+ );
+ const domSerialized = canonicalize(domSnapshot);
+
+ const a11yTree = await cdpSession.send("Accessibility.getFullAXTree" as any);
+ const a11ySerialized = canonicalize(a11yTree);
+
+ const screenshotResult = await cdpSession.send(
+ "Page.captureScreenshot" as any,
+ { format: "png" } as any,
+ ) as { data: string };
+ const screenshotBuffer = Buffer.from(screenshotResult.data, "base64");
+
+ const record: StepRecord = {
+ label: signal.label,
+ stepNumber: stepCount,
+ timestamp: new Date().toISOString(),
+ domHash: createHash("sha256").update(domSerialized).digest("hex"),
+ a11yHash: createHash("sha256").update(a11ySerialized).digest("hex"),
+ screenshotHash: createHash("sha256").update(screenshotBuffer).digest("hex"),
+ };
+
+ steps.push(record);
+ artifacts.set(stepCount, {
+ dom: domSerialized,
+ a11y: a11ySerialized,
+ screenshot: screenshotBuffer,
+ });
+
+ console.log(`[dbar-capture] Step ${stepCount} captured`);
+ } catch (err: unknown) {
+ const message = err instanceof Error ? err.message : String(err);
+ console.error(`[dbar-capture] Failed to capture step ${stepCount}: ${message}`);
+ }
+ } else {
+ console.log("[dbar-capture] Finish signal received.");
+ running = false;
+ }
+ }
+
+ const manifestPath = writeArtifacts(outputDir, steps, artifacts, cdpUrl);
+ console.log(`[dbar-capture] Manifest written to: ${manifestPath}`);
+ console.log(`[dbar-capture] Steps captured: ${steps.length}`);
+
+ await cdpSession.detach();
+ await browser.close();
+ process.exitCode = 0;
+}
diff --git a/integrations/browser-use/example.py b/integrations/browser-use/example.py
new file mode 100644
index 0000000..836a4cc
--- /dev/null
+++ b/integrations/browser-use/example.py
@@ -0,0 +1,105 @@
+"""
+DBAR + browser-use snapshot capture example.
+
+Runs a browser-use agent with DBAR capturing page state snapshots at each
+step boundary via the on_step_end lifecycle hook.
+
+Prerequisites:
+ python3.11 -m venv .venv && source .venv/bin/activate
+ pip install browser-use==0.12.5 langchain-openai==0.1.25
+ cd integrations/browser-use && npm install
+
+Environment:
+ OPENAI_API_KEY must be set (or swap ChatOpenAI for ChatAnthropic + ANTHROPIC_API_KEY)
+
+Usage:
+ python example.py
+"""
+
+import asyncio
+import json
+import subprocess
+import time
+from pathlib import Path
+
+from browser_use import Agent, Browser
+from langchain_openai import ChatOpenAI
+
+# Pin: browser-use==0.12.5, langchain-openai==0.1.25, cdp-use==1.4.5
+
+SIGNAL_DIR = Path(__file__).parent
+SNAPSHOTS_DIR = SIGNAL_DIR / "dbar-snapshots"
+
+
+async def on_step_end(agent) -> None:
+ """Signal DBAR sidecar to capture state at this step boundary."""
+ step_num = len(getattr(agent.history, "history", []))
+ target_id = getattr(agent.browser_session, "agent_focus_target_id", None)
+ payload = {
+ "label": f"step-{step_num}",
+ "targetId": target_id,
+ }
+ (SIGNAL_DIR / ".dbar-step").write_text(json.dumps(payload))
+ # Allow the capture sidecar time to detect and process the signal.
+ await asyncio.sleep(0.5)
+
+
+async def main() -> None:
+ # Start the browser up front so we can hand its real CDP URL to the DBAR sidecar.
+ browser = Browser(headless=False)
+ await browser.start()
+
+ cdp_url = getattr(browser, "cdp_url", None)
+ if not cdp_url:
+ raise RuntimeError("browser-use did not expose a CDP URL after browser.start()")
+
+ print(f"[example] Browser started at {cdp_url}")
+
+ agent = Agent(
+ task="Go to books.toscrape.com and find the price of the first Travel book",
+ llm=ChatOpenAI(model="gpt-4o"),
+ browser=browser,
+ )
+
+ print("[example] Starting DBAR capture sidecar...")
+ dbar_proc = subprocess.Popen(
+ [
+ "npx",
+ "tsx",
+ str(SIGNAL_DIR / "capture.ts"),
+ cdp_url,
+ str(SNAPSHOTS_DIR),
+ ],
+ cwd=str(SIGNAL_DIR),
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ )
+
+ # Give the sidecar time to connect via CDP.
+ time.sleep(3)
+
+ try:
+ print("[example] Running agent...")
+ result = await agent.run(
+ max_steps=20,
+ on_step_end=on_step_end,
+ )
+ print(f"[example] Agent result: {result}")
+ finally:
+ # Signal DBAR to finish and write manifest even if the run errors.
+ (SIGNAL_DIR / ".dbar-finish").touch()
+
+ try:
+ dbar_proc.wait(timeout=15)
+ finally:
+ if dbar_proc.stdout:
+ output = dbar_proc.stdout.read().decode()
+ print(f"[example] DBAR output:\n{output}")
+
+ await agent.close()
+
+ print(f"[example] Done. Check {SNAPSHOTS_DIR}/ for captured snapshots.")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/integrations/browser-use/package-lock.json b/integrations/browser-use/package-lock.json
new file mode 100644
index 0000000..3809b49
--- /dev/null
+++ b/integrations/browser-use/package-lock.json
@@ -0,0 +1,1555 @@
+{
+ "name": "@pyyush/dbar-browser-use",
+ "version": "0.2.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "@pyyush/dbar-browser-use",
+ "version": "0.2.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@pyyush/dbar": "file:../.."
+ },
+ "devDependencies": {
+ "@types/node": "22.19.15",
+ "playwright-core": "1.58.2",
+ "ts-node": "10.9.2",
+ "typescript": "5.9.3",
+ "vitest": "4.1.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "playwright-core": ">=1.40.0"
+ }
+ },
+ "../..": {
+ "name": "@pyyush/dbar",
+ "version": "0.2.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "zod": "^3.23.0"
+ },
+ "bin": {
+ "dbar": "dist/cli.js"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.37.0",
+ "@types/node": "^25.5.0",
+ "@typescript-eslint/eslint-plugin": "^8.46.1",
+ "@typescript-eslint/parser": "^8.46.1",
+ "eslint": "^9.37.0",
+ "globals": "^16.4.0",
+ "playwright-core": "^1.52.0",
+ "tsup": "^8.3.0",
+ "typescript": "^5.7.0",
+ "vitest": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "playwright-core": ">=1.40.0"
+ },
+ "peerDependenciesMeta": {
+ "playwright-core": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/@cspotcode/source-map-support": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
+ "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "0.3.9"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz",
+ "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.0",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz",
+ "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
+ "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
+ "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.0.3",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ }
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
+ "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1",
+ "@tybys/wasm-util": "^0.10.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.122.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz",
+ "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@pyyush/dbar": {
+ "resolved": "../..",
+ "link": true
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz",
+ "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz",
+ "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz",
+ "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz",
+ "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz",
+ "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz",
+ "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz",
+ "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz",
+ "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz",
+ "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz",
+ "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz",
+ "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz",
+ "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz",
+ "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@napi-rs/wasm-runtime": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz",
+ "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz",
+ "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz",
+ "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node10": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
+ "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node12": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
+ "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node14": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
+ "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node16": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
+ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
+ "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "22.19.15",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
+ "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@vitest/expect": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz",
+ "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.1.0",
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "4.1.2",
+ "@vitest/utils": "4.1.2",
+ "chai": "^6.2.2",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz",
+ "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "4.1.2",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.21"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz",
+ "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz",
+ "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "4.1.2",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz",
+ "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.2",
+ "@vitest/utils": "4.1.2",
+ "magic-string": "^0.30.21",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz",
+ "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz",
+ "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.2",
+ "convert-source-map": "^2.0.0",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-walk": {
+ "version": "8.3.5",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz",
+ "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.11.0"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/arg": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/chai": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
+ "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/create-require": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
+ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/diff": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
+ "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
+ "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
+ "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/make-error": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/obug": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
+ "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT"
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.58.2",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
+ "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.8",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
+ "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz",
+ "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.122.0",
+ "@rolldown/pluginutils": "1.0.0-rc.12"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.0.0-rc.12",
+ "@rolldown/binding-darwin-arm64": "1.0.0-rc.12",
+ "@rolldown/binding-darwin-x64": "1.0.0-rc.12",
+ "@rolldown/binding-freebsd-x64": "1.0.0-rc.12",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12",
+ "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12",
+ "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12",
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12",
+ "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12",
+ "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12",
+ "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12",
+ "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12",
+ "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12",
+ "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12",
+ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12"
+ }
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/std-env": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz",
+ "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz",
+ "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
+ "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/ts-node": {
+ "version": "10.9.2",
+ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
+ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@cspotcode/source-map-support": "^0.8.0",
+ "@tsconfig/node10": "^1.0.7",
+ "@tsconfig/node12": "^1.0.7",
+ "@tsconfig/node14": "^1.0.0",
+ "@tsconfig/node16": "^1.0.2",
+ "acorn": "^8.4.1",
+ "acorn-walk": "^8.1.1",
+ "arg": "^4.1.0",
+ "create-require": "^1.1.0",
+ "diff": "^4.0.1",
+ "make-error": "^1.1.1",
+ "v8-compile-cache-lib": "^3.0.1",
+ "yn": "3.1.1"
+ },
+ "bin": {
+ "ts-node": "dist/bin.js",
+ "ts-node-cwd": "dist/bin-cwd.js",
+ "ts-node-esm": "dist/bin-esm.js",
+ "ts-node-script": "dist/bin-script.js",
+ "ts-node-transpile-only": "dist/bin-transpile.js",
+ "ts-script": "dist/bin-script-deprecated.js"
+ },
+ "peerDependencies": {
+ "@swc/core": ">=1.2.50",
+ "@swc/wasm": ">=1.2.50",
+ "@types/node": "*",
+ "typescript": ">=2.7"
+ },
+ "peerDependenciesMeta": {
+ "@swc/core": {
+ "optional": true
+ },
+ "@swc/wasm": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/v8-compile-cache-lib": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
+ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vite": {
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz",
+ "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.4",
+ "postcss": "^8.5.8",
+ "rolldown": "1.0.0-rc.12",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.1.0",
+ "esbuild": "^0.27.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz",
+ "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "4.1.2",
+ "@vitest/mocker": "4.1.2",
+ "@vitest/pretty-format": "4.1.2",
+ "@vitest/runner": "4.1.2",
+ "@vitest/snapshot": "4.1.2",
+ "@vitest/spy": "4.1.2",
+ "@vitest/utils": "4.1.2",
+ "es-module-lexer": "^2.0.0",
+ "expect-type": "^1.3.0",
+ "magic-string": "^0.30.21",
+ "obug": "^2.1.1",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.3",
+ "std-env": "^4.0.0-rc.1",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^1.0.2",
+ "tinyglobby": "^0.2.15",
+ "tinyrainbow": "^3.1.0",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "4.1.2",
+ "@vitest/browser-preview": "4.1.2",
+ "@vitest/browser-webdriverio": "4.1.2",
+ "@vitest/ui": "4.1.2",
+ "happy-dom": "*",
+ "jsdom": "*",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ },
+ "vite": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yn": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+ "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ }
+ }
+}
diff --git a/integrations/browser-use/package.json b/integrations/browser-use/package.json
new file mode 100644
index 0000000..91c1b08
--- /dev/null
+++ b/integrations/browser-use/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "@pyyush/dbar-browser-use",
+ "version": "0.2.0",
+ "description": "DBAR snapshot capture sidecar for browser-use agent sessions",
+ "private": true,
+ "author": "Piyush Vyas",
+ "license": "Apache-2.0",
+ "type": "module",
+ "scripts": {
+ "build": "tsc",
+ "capture": "node --loader ts-node/esm capture.ts",
+ "prepublishOnly": "node -e \"console.error('This integration package is intentionally not publishable.'); process.exit(1)\"",
+ "test": "vitest run",
+ "test:watch": "vitest",
+ "typecheck": "tsc --noEmit"
+ },
+ "dependencies": {
+ "@pyyush/dbar": "file:../.."
+ },
+ "peerDependencies": {
+ "playwright-core": ">=1.40.0"
+ },
+ "devDependencies": {
+ "@types/node": "22.19.15",
+ "playwright-core": "1.58.2",
+ "ts-node": "10.9.2",
+ "typescript": "5.9.3",
+ "vitest": "4.1.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+}
diff --git a/integrations/browser-use/requirements.txt b/integrations/browser-use/requirements.txt
new file mode 100644
index 0000000..d25c82b
--- /dev/null
+++ b/integrations/browser-use/requirements.txt
@@ -0,0 +1,2 @@
+browser-use==0.12.5
+langchain-openai==0.1.25
diff --git a/integrations/browser-use/tsconfig.json b/integrations/browser-use/tsconfig.json
new file mode 100644
index 0000000..5dadbe4
--- /dev/null
+++ b/integrations/browser-use/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "lib": ["ES2022"],
+ "outDir": "./dist",
+ "declaration": true,
+ "sourceMap": true,
+ "strict": true,
+ "noImplicitAny": true,
+ "strictNullChecks": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedIndexedAccess": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true
+ },
+ "include": ["*.ts"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/integrations/browser-use/vitest.config.ts b/integrations/browser-use/vitest.config.ts
new file mode 100644
index 0000000..8fa7fcd
--- /dev/null
+++ b/integrations/browser-use/vitest.config.ts
@@ -0,0 +1,9 @@
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ test: {
+ globals: true,
+ environment: "node",
+ include: ["*.test.ts"],
+ },
+});
diff --git a/integrations/browserbase/README.md b/integrations/browserbase/README.md
new file mode 100644
index 0000000..6b3082e
--- /dev/null
+++ b/integrations/browserbase/README.md
@@ -0,0 +1,161 @@
+# DBAR Browserbase Integration
+
+First-class DBAR integration for [Browserbase](https://www.browserbase.com/)
+sessions.
+
+Use this lane when you want DBAR to own a Browserbase-hosted browser session
+end to end and keep full deterministic capture, replay, and first-divergence
+diagnosis.
+
+Compared with the `browser-use` integration, Browserbase is the full-control
+lane: DBAR owns the session, records network, freezes time, and produces a
+replayable capsule.
+
+This integration is verified against these exact versions:
+
+- Node.js 20+
+- `@browserbasehq/sdk==2.9.0`
+- `playwright-core==1.58.2`
+- `tsx==4.21.0`
+- `typescript==5.9.3`
+- `vitest==4.1.2`
+
+**DBAR owns the Browserbase session.** Unlike the browser-use integration (where DBAR is a sidecar observing someone else's browser), here DBAR controls the session end-to-end. This means full deterministic capture works: virtual time, network recording, and replayable capsules.
+
+| | browser-use integration | Browserbase integration |
+|---|---|---|
+| Who owns the browser? | browser-use (agent) | DBAR |
+| Full determinism? | No (CDP conflict) | Yes |
+| Virtual time? | No | Yes |
+| Network recording? | No | Yes |
+| Replayable capsule? | No (snapshots only) | Yes |
+| Value prop | Audit trail of page state | Full deterministic record + replay |
+
+## Setup
+
+```bash
+cd integrations/browserbase
+npm install
+```
+
+### Credentials
+
+Auth is via environment variables only. Never pass secrets as CLI flags.
+
+```bash
+export BROWSERBASE_API_KEY=your-api-key
+export BROWSERBASE_PROJECT_ID=your-project-id
+```
+
+### Pinned Versions
+
+- `@browserbasehq/sdk` 2.9.0 (uses `session.connectUrl` for CDP)
+- `playwright-core` 1.58.2
+- `tsx` 4.21.0
+- `typescript` 5.9.3
+- `vitest` 4.1.2
+
+## Capture
+
+Record a page with full determinism in a Browserbase cloud browser:
+
+```bash
+npx tsx capture.ts --url https://books.toscrape.com/ --steps 3 --output ./capsules/demo.capsule
+```
+
+| Flag | Description | Default |
+|------|-------------|---------|
+| `--url` | URL to navigate to and capture | (required) |
+| `--steps` | Number of steps to capture | 1 |
+| `--output` | Capsule output path | `./capsules/.capsule` |
+
+The capture script:
+1. Creates a Browserbase session via the SDK
+2. Connects via `session.connectUrl` (CDP WebSocket)
+3. Navigates to the target URL
+4. Runs DBAR.capture() with full virtual time + network recording
+5. Captures the specified number of steps
+6. Saves the capsule to disk
+7. Closes the session
+
+The `connectUrl` contains the API key as a query parameter. All log output masks this value automatically.
+
+## Replay
+
+Replay a captured capsule on a local browser. No Browserbase credentials needed.
+
+```bash
+npx tsx replay.ts ./capsules/demo.capsule
+npx tsx replay.ts ./capsules/demo.capsule --json
+```
+
+| Flag | Description | Default |
+|------|-------------|---------|
+| `` | Path to capsule file | (required) |
+| `--json` | Output structured JSON to stdout | false |
+
+Exit codes: 0 = all steps matched, 1 = divergences detected, 2 = fatal error.
+
+Set `DBAR_NO_SANDBOX=1` to add `--no-sandbox` to the local Chromium launch (CI environments only).
+
+## Example
+
+Full end-to-end demo: create session, browse books.toscrape.com, capture 3 steps, replay locally.
+
+```bash
+npx tsx example.ts
+```
+
+## Programmatic Usage
+
+```typescript
+import Browserbase from "@browserbasehq/sdk";
+import { chromium } from "playwright-core";
+import { DBAR, serializeCapsuleArchive } from "@pyyush/dbar";
+
+const bb = new Browserbase({ apiKey: process.env.BROWSERBASE_API_KEY! });
+
+const session = await bb.sessions.create({
+ projectId: process.env.BROWSERBASE_PROJECT_ID!,
+});
+
+const browser = await chromium.connectOverCDP(session.connectUrl);
+const page = browser.contexts()[0].pages()[0];
+
+// DBAR owns the session — full determinism
+const dbar = await DBAR.capture(page);
+await page.goto("https://example.com");
+await dbar.step("homepage");
+const archive = await dbar.finish();
+
+// Save capsule
+const capsule = serializeCapsuleArchive(archive);
+
+// Later: replay locally (no Browserbase needed)
+const result = await DBAR.replay(freshPage, archive);
+console.log(result.replaySuccessRate); // 1.0
+```
+
+## Security Notice
+
+Capsules contain full network response bodies, cookies, localStorage values, and screenshots. These may include session tokens, PII, or other sensitive data. Treat capsule files with the same care as database backups.
+
+- Do not commit capsules to public repositories
+- DBAR redacts auth headers by default
+- Review capsule contents before sharing
+
+## Files
+
+| File | Description |
+|------|-------------|
+| `capture.ts` | CLI: create Browserbase session, capture deterministic capsule |
+| `replay.ts` | CLI: replay capsule locally, output results |
+| `example.ts` | End-to-end demo (capture on Browserbase, replay locally) |
+| `helpers.ts` | Pure helper functions (arg parsing, URL masking) |
+| `package.json` | Dependencies (pins @browserbasehq/sdk 2.9.0) |
+| `tsconfig.json` | TypeScript configuration |
+| `__tests__/` | Unit tests for helper functions |
+
+## License
+
+Apache-2.0
diff --git a/integrations/browserbase/__tests__/helpers.test.ts b/integrations/browserbase/__tests__/helpers.test.ts
new file mode 100644
index 0000000..8dfa368
--- /dev/null
+++ b/integrations/browserbase/__tests__/helpers.test.ts
@@ -0,0 +1,130 @@
+import { describe, it, expect } from "vitest";
+import { maskConnectUrl, parseCaptureArgs, parseReplayArgs } from "../helpers.js";
+
+describe("maskConnectUrl", () => {
+ it("should mask apiKey query parameter in connectUrl", () => {
+ const url =
+ "wss://connect.browserbase.com?sessionId=abc123&apiKey=sk-secret-key-value";
+
+ const masked = maskConnectUrl(url);
+
+ expect(masked).toBe(
+ "wss://connect.browserbase.com?sessionId=abc123&apiKey=[MASKED]"
+ );
+ });
+
+ it("should return url unchanged when no apiKey parameter exists", () => {
+ const url = "wss://connect.browserbase.com?sessionId=abc123";
+
+ const masked = maskConnectUrl(url);
+
+ expect(masked).toBe(url);
+ });
+
+ it("should mask apiKey regardless of parameter position", () => {
+ const url =
+ "wss://connect.browserbase.com?apiKey=secret&sessionId=abc123&other=val";
+
+ const masked = maskConnectUrl(url);
+
+ expect(masked).toContain("apiKey=[MASKED]");
+ expect(masked).toContain("sessionId=abc123");
+ expect(masked).toContain("other=val");
+ expect(masked).not.toContain("secret");
+ });
+
+ it("should handle malformed URLs by returning the original string", () => {
+ const notAUrl = "not-a-url-at-all";
+
+ const masked = maskConnectUrl(notAUrl);
+
+ expect(masked).toBe(notAUrl);
+ });
+});
+
+describe("parseCaptureArgs", () => {
+ it("should parse --url flag", () => {
+ const result = parseCaptureArgs(["--url", "https://example.com"]);
+
+ expect(result.url).toBe("https://example.com");
+ });
+
+ it("should parse --steps flag as number", () => {
+ const result = parseCaptureArgs(["--url", "https://example.com", "--steps", "5"]);
+
+ expect(result.steps).toBe(5);
+ });
+
+ it("should default steps to 1 when not provided", () => {
+ const result = parseCaptureArgs(["--url", "https://example.com"]);
+
+ expect(result.steps).toBe(1);
+ });
+
+ it("should parse --output flag", () => {
+ const result = parseCaptureArgs([
+ "--url",
+ "https://example.com",
+ "--output",
+ "/tmp/my.capsule",
+ ]);
+
+ expect(result.output).toBe("/tmp/my.capsule");
+ });
+
+ it("should generate default output path when --output not provided", () => {
+ const result = parseCaptureArgs(["--url", "https://example.com"]);
+
+ expect(result.output).toMatch(/^.*\/capsules\/\d+\.capsule$/);
+ });
+
+ it("should return error when --url is missing", () => {
+ const result = parseCaptureArgs(["--steps", "3"]);
+
+ expect(result.error).toBe("--url is required");
+ });
+
+ it("should return error when --steps is not a positive integer", () => {
+ const result = parseCaptureArgs(["--url", "https://example.com", "--steps", "0"]);
+
+ expect(result.error).toContain("--steps must be a positive integer");
+ });
+
+ it("should return error when --steps is not a number", () => {
+ const result = parseCaptureArgs(["--url", "https://example.com", "--steps", "abc"]);
+
+ expect(result.error).toContain("--steps must be a positive integer");
+ });
+});
+
+describe("parseReplayArgs", () => {
+ it("should parse capsule path as first positional argument", () => {
+ const result = parseReplayArgs(["/path/to/capsule.capsule"]);
+
+ expect(result.capsulePath).toBe("/path/to/capsule.capsule");
+ });
+
+ it("should parse --json flag", () => {
+ const result = parseReplayArgs(["/path/to/capsule.capsule", "--json"]);
+
+ expect(result.json).toBe(true);
+ });
+
+ it("should default json to false", () => {
+ const result = parseReplayArgs(["/path/to/capsule.capsule"]);
+
+ expect(result.json).toBe(false);
+ });
+
+ it("should return error when capsule path is missing", () => {
+ const result = parseReplayArgs([]);
+
+ expect(result.error).toBe("capsule path is required");
+ });
+
+ it("should return error when capsule path is missing but --json is present", () => {
+ const result = parseReplayArgs(["--json"]);
+
+ expect(result.error).toBe("capsule path is required");
+ });
+});
diff --git a/integrations/browserbase/capture.ts b/integrations/browserbase/capture.ts
new file mode 100644
index 0000000..2513aa5
--- /dev/null
+++ b/integrations/browserbase/capture.ts
@@ -0,0 +1,125 @@
+/**
+ * DBAR Capture via Browserbase SDK
+ *
+ * Creates a Browserbase cloud browser session, connects DBAR via CDP, and
+ * records a determinism capsule with full virtual time + network recording.
+ *
+ * Unlike the browser-use integration (where DBAR is a sidecar), here DBAR
+ * OWNS the browser session — so full deterministic capture is possible.
+ *
+ * Usage:
+ * npx tsx capture.ts --url [--steps ] [--output ]
+ *
+ * Auth: BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID env vars (no CLI flags for secrets).
+ *
+ * @module
+ */
+
+import { mkdirSync, writeFileSync } from "node:fs";
+import { dirname } from "node:path";
+import Browserbase from "@browserbasehq/sdk";
+import { chromium } from "playwright-core";
+import { DBAR, serializeCapsuleArchive } from "@pyyush/dbar";
+
+import { maskConnectUrl, parseCaptureArgs } from "./helpers.js";
+
+/**
+ * Patch page.accessibility for modern Playwright versions where the
+ * property was removed in favor of page.accessibility being undefined.
+ * DBAR's accessibility snapshot uses this internally.
+ */
+function patchAccessibility(page: import("playwright-core").Page): void {
+ if (!page.accessibility) {
+ Object.defineProperty(page, "accessibility", {
+ value: {
+ async snapshot() {
+ return page.evaluate(() => {
+ // Fallback: return minimal tree so DBAR can still hash something
+ return { role: "WebArea", name: document.title, children: [] };
+ });
+ },
+ },
+ });
+ }
+}
+
+async function main(): Promise {
+ const args = parseCaptureArgs(process.argv.slice(2));
+ if (args.error) {
+ console.error(`[dbar-capture] Error: ${args.error}`);
+ console.error("[dbar-capture] Usage: npx tsx capture.ts --url [--steps ] [--output ]");
+ process.exit(1);
+ }
+
+ const apiKey = process.env["BROWSERBASE_API_KEY"];
+ const projectId = process.env["BROWSERBASE_PROJECT_ID"];
+
+ if (!apiKey) {
+ console.error("[dbar-capture] Missing BROWSERBASE_API_KEY environment variable.");
+ console.error("[dbar-capture] Set it with: export BROWSERBASE_API_KEY=");
+ process.exit(1);
+ }
+
+ if (!projectId) {
+ console.error("[dbar-capture] Missing BROWSERBASE_PROJECT_ID environment variable.");
+ console.error("[dbar-capture] Set it with: export BROWSERBASE_PROJECT_ID=");
+ process.exit(1);
+ }
+
+ const bb = new Browserbase({ apiKey });
+
+ console.log("[dbar-capture] Creating Browserbase session...");
+ const session = await bb.sessions.create({
+ projectId,
+ browserSettings: {
+ blockAds: true,
+ solveCaptchas: true,
+ viewport: { width: 1920, height: 1080 },
+ },
+ });
+
+ console.log(`[dbar-capture] Session created: ${session.id}`);
+ console.log(`[dbar-capture] Connect URL: ${maskConnectUrl(session.connectUrl)}`);
+
+ let browser: Awaited> | undefined;
+ try {
+ browser = await chromium.connectOverCDP(session.connectUrl);
+ const page = browser.contexts()[0]!.pages()[0]!;
+
+ patchAccessibility(page);
+
+ console.log(`[dbar-capture] Navigating to ${args.url!}...`);
+ await page.goto(args.url!, { waitUntil: "networkidle" });
+
+ console.log("[dbar-capture] Starting DBAR capture (full determinism: virtual time + network)...");
+ const dbar = await DBAR.capture(page);
+
+ for (let i = 0; i < args.steps; i++) {
+ const label = `step-${i}`;
+ console.log(`[dbar-capture] Capturing step ${i + 1}/${args.steps}: ${label}`);
+ await dbar.step(label);
+ }
+
+ const archive = await dbar.finish();
+ const capsule = serializeCapsuleArchive(archive);
+
+ mkdirSync(dirname(args.output), { recursive: true });
+ writeFileSync(args.output, capsule, "utf-8");
+
+ console.log(`[dbar-capture] Capsule written to: ${args.output}`);
+ console.log(
+ `[dbar-capture] Steps: ${archive.manifest.steps.length}, ` +
+ `Requests: ${archive.manifest.networkTranscript.entries.length}`
+ );
+ } finally {
+ if (browser) {
+ await browser.close();
+ }
+ console.log("[dbar-capture] Session closed.");
+ }
+}
+
+main().catch((error: unknown) => {
+ console.error("[dbar-capture] Fatal error:", error);
+ process.exit(1);
+});
diff --git a/integrations/browserbase/example.ts b/integrations/browserbase/example.ts
new file mode 100644
index 0000000..72bcd1c
--- /dev/null
+++ b/integrations/browserbase/example.ts
@@ -0,0 +1,183 @@
+/**
+ * DBAR + Browserbase End-to-End Example
+ *
+ * Demonstrates the full flow:
+ * 1. Create Browserbase session via SDK
+ * 2. Navigate to books.toscrape.com
+ * 3. DBAR captures with full determinism (virtual time + network)
+ * 4. Browse a few pages (click category, click book)
+ * 5. Finish capture, save capsule
+ * 6. Replay locally, show results
+ * 7. Clean up session
+ *
+ * Prerequisites:
+ * export BROWSERBASE_API_KEY=your-api-key
+ * export BROWSERBASE_PROJECT_ID=your-project-id
+ *
+ * Usage:
+ * npx tsx example.ts
+ *
+ * @module
+ */
+
+import { mkdirSync, writeFileSync, readFileSync } from "node:fs";
+import { resolve, join } from "node:path";
+import Browserbase from "@browserbasehq/sdk";
+import { chromium } from "playwright-core";
+import { DBAR, serializeCapsuleArchive, deserializeCapsuleArchive } from "@pyyush/dbar";
+
+import { maskConnectUrl } from "./helpers.js";
+
+const CAPSULES_DIR = resolve("./capsules");
+
+/** Resolve required environment variable or exit with an actionable message. */
+function requireEnv(name: string): string {
+ const value = process.env[name];
+ if (!value) {
+ console.error(`[example] Missing required environment variable: ${name}`);
+ console.error(`[example] Set it with: export ${name}=`);
+ process.exit(1);
+ }
+ return value;
+}
+
+async function main(): Promise {
+ const apiKey = requireEnv("BROWSERBASE_API_KEY");
+ const projectId = requireEnv("BROWSERBASE_PROJECT_ID");
+
+ // -------------------------------------------------------------------------
+ // 1. Create a Browserbase session via SDK
+ // -------------------------------------------------------------------------
+ const bb = new Browserbase({ apiKey });
+
+ console.log("[example] Creating Browserbase session...");
+ const session = await bb.sessions.create({
+ projectId,
+ browserSettings: {
+ blockAds: true,
+ solveCaptchas: true,
+ viewport: { width: 1920, height: 1080 },
+ },
+ });
+ console.log(`[example] Session created: ${session.id}`);
+ console.log(`[example] Connect URL: ${maskConnectUrl(session.connectUrl)}`);
+
+ let browser: Awaited> | undefined;
+ try {
+ // -----------------------------------------------------------------------
+ // 2. Connect via CDP
+ // -----------------------------------------------------------------------
+ browser = await chromium.connectOverCDP(session.connectUrl);
+ const page = browser.contexts()[0]!.pages()[0]!;
+
+ // -----------------------------------------------------------------------
+ // 3. Navigate and start DBAR capture
+ // -----------------------------------------------------------------------
+ console.log("[example] Navigating to books.toscrape.com...");
+ await page.goto("https://books.toscrape.com/", { waitUntil: "networkidle" });
+
+ console.log("[example] Starting DBAR capture (full determinism)...");
+ const dbarSession = await DBAR.capture(page);
+
+ // Step 0: Homepage loaded
+ await dbarSession.step("homepage");
+ console.log("[example] Step 1 captured: homepage");
+
+ // -----------------------------------------------------------------------
+ // 4. Browse — click a category, then a book
+ // -----------------------------------------------------------------------
+ const categoryLink = page.locator("aside .nav-list ul a").first();
+ if (await categoryLink.count() > 0) {
+ console.log("[example] Clicking first category...");
+ await categoryLink.click();
+ await page.waitForLoadState("networkidle");
+ await dbarSession.step("category-page");
+ console.log("[example] Step 2 captured: category-page");
+ }
+
+ const bookLink = page.locator("article.product_pod h3 a").first();
+ if (await bookLink.count() > 0) {
+ console.log("[example] Clicking first book...");
+ await bookLink.click();
+ await page.waitForLoadState("networkidle");
+ await dbarSession.step("book-detail");
+ console.log("[example] Step 3 captured: book-detail");
+ }
+
+ // -----------------------------------------------------------------------
+ // 5. Finish capture, save capsule
+ // -----------------------------------------------------------------------
+ console.log("[example] Finishing capture...");
+ const archive = await dbarSession.finish();
+
+ mkdirSync(CAPSULES_DIR, { recursive: true });
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
+ const capsulePath = join(CAPSULES_DIR, `example-${timestamp}.capsule`);
+ const serialized = serializeCapsuleArchive(archive);
+ writeFileSync(capsulePath, serialized, "utf-8");
+
+ console.log(`[example] Capsule written to: ${capsulePath}`);
+ console.log(
+ `[example] Steps: ${archive.manifest.steps.length}, ` +
+ `Requests: ${archive.manifest.networkTranscript.entries.length}`
+ );
+
+ // Close cloud browser before local replay
+ await browser.close();
+ browser = undefined;
+
+ // -----------------------------------------------------------------------
+ // 6. Replay locally
+ // -----------------------------------------------------------------------
+ console.log("[example] Replaying capsule locally...");
+
+ const replaySerialized = readFileSync(capsulePath, "utf-8");
+ const replayArchive = deserializeCapsuleArchive(replaySerialized);
+ const manifest = replayArchive.manifest;
+
+ const noSandbox = process.env["DBAR_NO_SANDBOX"] === "1";
+ const localBrowser = await chromium.launch({
+ headless: true,
+ args: [
+ "--disable-gpu",
+ ...(noSandbox ? ["--no-sandbox"] : []),
+ ],
+ });
+
+ const localContext = await localBrowser.newContext({
+ viewport: {
+ width: manifest.environment.viewport.width,
+ height: manifest.environment.viewport.height,
+ },
+ locale: manifest.environment.locale,
+ timezoneId: manifest.environment.timezone,
+ userAgent: manifest.environment.userAgent,
+ });
+
+ const localPage = await localContext.newPage();
+ const result = await DBAR.replay(localPage, replayArchive);
+
+ console.log("[example] Replay complete:");
+ console.log(`[example] Success: ${result.success}`);
+ console.log(`[example] RSR: ${(result.replaySuccessRate * 100).toFixed(1)}%`);
+ console.log(`[example] DVR: ${(result.determinismViolationRate * 100).toFixed(1)}%`);
+ console.log(`[example] Divergences: ${result.divergences.length}`);
+ console.log(`[example] Overhead: ${result.overheadMs}ms`);
+
+ await localBrowser.close();
+
+ // -----------------------------------------------------------------------
+ // 7. Done
+ // -----------------------------------------------------------------------
+ process.exit(result.success ? 0 : 1);
+ } finally {
+ if (browser) {
+ await browser.close();
+ }
+ }
+}
+
+main().catch((error: unknown) => {
+ console.error("[example] Fatal error:", error);
+ process.exit(2);
+});
diff --git a/integrations/browserbase/helpers.ts b/integrations/browserbase/helpers.ts
new file mode 100644
index 0000000..1689f91
--- /dev/null
+++ b/integrations/browserbase/helpers.ts
@@ -0,0 +1,123 @@
+/**
+ * Pure helper functions for the DBAR + Browserbase integration.
+ *
+ * Extracted from CLI scripts so they can be tested hermetically
+ * without network or filesystem dependencies.
+ *
+ * @module
+ */
+
+import { resolve, join } from "node:path";
+
+/**
+ * Mask the apiKey query parameter in a Browserbase connectUrl.
+ *
+ * connectUrl contains the API key as a query parameter (e.g.,
+ * `wss://connect.browserbase.com?sessionId=...&apiKey=sk-...`).
+ * This function replaces the apiKey value with `[MASKED]` to prevent
+ * accidental leakage in log output.
+ *
+ * @param url - The connectUrl string (may or may not contain apiKey)
+ * @returns The URL with apiKey value replaced, or the original string if parsing fails
+ */
+export function maskConnectUrl(url: string): string {
+ // Use regex instead of URL API to avoid encoding artifacts
+ // (URL encodes brackets in query values as %5B/%5D)
+ return url.replace(/([?&]apiKey=)[^&]+/, "$1[MASKED]");
+}
+
+/** Result of parsing capture CLI arguments. */
+export interface CaptureArgs {
+ url?: string;
+ steps: number;
+ output: string;
+ error?: string;
+}
+
+/**
+ * Parse CLI arguments for the capture script.
+ *
+ * @param argv - Raw argument strings (without node and script path)
+ * @returns Parsed arguments, or an error message if validation fails
+ *
+ * @example
+ * ```ts
+ * const args = parseCaptureArgs(["--url", "https://example.com", "--steps", "3"]);
+ * if (args.error) { console.error(args.error); process.exit(1); }
+ * ```
+ */
+export function parseCaptureArgs(argv: string[]): CaptureArgs {
+ let url: string | undefined;
+ let steps = 1;
+ let output: string | undefined;
+ let error: string | undefined;
+
+ for (let i = 0; i < argv.length; i++) {
+ const arg = argv[i];
+ const next = argv[i + 1];
+
+ if (arg === "--url" && next) {
+ url = next;
+ i++;
+ } else if (arg === "--steps" && next) {
+ const parsed = Number(next);
+ if (!Number.isInteger(parsed) || parsed < 1) {
+ error = "--steps must be a positive integer";
+ } else {
+ steps = parsed;
+ }
+ i++;
+ } else if (arg === "--output" && next) {
+ output = next;
+ i++;
+ }
+ }
+
+ if (!url && !error) {
+ error = "--url is required";
+ }
+
+ if (!output) {
+ output = join(resolve("./capsules"), `${Date.now()}.capsule`);
+ }
+
+ return { url, steps, output, error };
+}
+
+/** Result of parsing replay CLI arguments. */
+export interface ReplayArgs {
+ capsulePath?: string;
+ json: boolean;
+ error?: string;
+}
+
+/**
+ * Parse CLI arguments for the replay script.
+ *
+ * @param argv - Raw argument strings (without node and script path)
+ * @returns Parsed arguments, or an error message if validation fails
+ *
+ * @example
+ * ```ts
+ * const args = parseReplayArgs(["./capsule.capsule", "--json"]);
+ * if (args.error) { console.error(args.error); process.exit(1); }
+ * ```
+ */
+export function parseReplayArgs(argv: string[]): ReplayArgs {
+ let capsulePath: string | undefined;
+ let json = false;
+
+ for (const arg of argv) {
+ if (arg === "--json") {
+ json = true;
+ } else if (!arg.startsWith("--")) {
+ capsulePath = arg;
+ }
+ }
+
+ if (!capsulePath) {
+ return { json, error: "capsule path is required" };
+ }
+
+ return { capsulePath, json };
+}
diff --git a/integrations/browserbase/package-lock.json b/integrations/browserbase/package-lock.json
new file mode 100644
index 0000000..a43a175
--- /dev/null
+++ b/integrations/browserbase/package-lock.json
@@ -0,0 +1,2325 @@
+{
+ "name": "@pyyush/dbar-browserbase",
+ "version": "0.2.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "@pyyush/dbar-browserbase",
+ "version": "0.2.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@browserbasehq/sdk": "2.9.0",
+ "@pyyush/dbar": "file:../.."
+ },
+ "devDependencies": {
+ "@types/node": "22.19.15",
+ "playwright-core": "1.58.2",
+ "tsx": "4.21.0",
+ "typescript": "5.9.3",
+ "vitest": "4.1.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "playwright-core": ">=1.40.0"
+ }
+ },
+ "../..": {
+ "name": "@pyyush/dbar",
+ "version": "0.2.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "zod": "^3.23.0"
+ },
+ "bin": {
+ "dbar": "dist/cli.js"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.37.0",
+ "@types/node": "^25.5.0",
+ "@typescript-eslint/eslint-plugin": "^8.46.1",
+ "@typescript-eslint/parser": "^8.46.1",
+ "eslint": "^9.37.0",
+ "globals": "^16.4.0",
+ "playwright-core": "^1.52.0",
+ "tsup": "^8.3.0",
+ "typescript": "^5.7.0",
+ "vitest": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "playwright-core": ">=1.40.0"
+ },
+ "peerDependenciesMeta": {
+ "playwright-core": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/@browserbasehq/sdk": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/@browserbasehq/sdk/-/sdk-2.9.0.tgz",
+ "integrity": "sha512-Xzm1+6suzQypXjley4Phqer++pjnYyST6S7CArUn3kWyGA8aruXjAV5wkmqE21lgXo9K3/OQJvCu48bKEZFNDQ==",
+ "dependencies": {
+ "@types/node": "^18.11.18",
+ "@types/node-fetch": "^2.6.4",
+ "abort-controller": "^3.0.0",
+ "agentkeepalive": "^4.2.1",
+ "form-data-encoder": "1.7.2",
+ "formdata-node": "^4.3.2",
+ "node-fetch": "^2.6.7"
+ }
+ },
+ "node_modules/@browserbasehq/sdk/node_modules/@types/node": {
+ "version": "18.19.130",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
+ "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~5.26.4"
+ }
+ },
+ "node_modules/@browserbasehq/sdk/node_modules/undici-types": {
+ "version": "5.26.5",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
+ "license": "MIT"
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz",
+ "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.0",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz",
+ "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
+ "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.27.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.5.tgz",
+ "integrity": "sha512-nGsF/4C7uzUj+Nj/4J+Zt0bYQ6bz33Phz8Lb2N80Mti1HjGclTJdXZ+9APC4kLvONbjxN1zfvYNd8FEcbBK/MQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.27.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.5.tgz",
+ "integrity": "sha512-Cv781jd0Rfj/paoNrul1/r4G0HLvuFKYh7C9uHZ2Pl8YXstzvCyyeWENTFR9qFnRzNMCjXmsulZuvosDg10Mog==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.27.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.5.tgz",
+ "integrity": "sha512-Oeghq+XFgh1pUGd1YKs4DDoxzxkoUkvko+T/IVKwlghKLvvjbGFB3ek8VEDBmNvqhwuL0CQS3cExdzpmUyIrgA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.27.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.5.tgz",
+ "integrity": "sha512-nQD7lspbzerlmtNOxYMFAGmhxgzn8Z7m9jgFkh6kpkjsAhZee1w8tJW3ZlW+N9iRePz0oPUDrYrXidCPSImD0Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.27.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.5.tgz",
+ "integrity": "sha512-I+Ya/MgC6rr8oRWGRDF3BXDfP8K1BVUggHqN6VI2lUZLdDi1IM1v2cy0e3lCPbP+pVcK3Tv8cgUhHse1kaNZZw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.27.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.5.tgz",
+ "integrity": "sha512-MCjQUtC8wWJn/pIPM7vQaO69BFgwPD1jriEdqwTCKzWjGgkMbcg+M5HzrOhPhuYe1AJjXlHmD142KQf+jnYj8A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.27.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.5.tgz",
+ "integrity": "sha512-X6xVS+goSH0UelYXnuf4GHLwpOdc8rgK/zai+dKzBMnncw7BTQIwquOodE7EKvY2UVUetSqyAfyZC1D+oqLQtg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.27.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.5.tgz",
+ "integrity": "sha512-233X1FGo3a8x1ekLB6XT69LfZ83vqz+9z3TSEQCTYfMNY880A97nr81KbPcAMl9rmOFp11wO0dP+eB18KU/Ucg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.27.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.5.tgz",
+ "integrity": "sha512-0wkVrYHG4sdCCN/bcwQ7yYMXACkaHc3UFeaEOwSVW6e5RycMageYAFv+JS2bKLwHyeKVUvtoVH+5/RHq0fgeFw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.27.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.5.tgz",
+ "integrity": "sha512-euKkilsNOv7x/M1NKsx5znyprbpsRFIzTV6lWziqJch7yWYayfLtZzDxDTl+LSQDJYAjd9TVb/Kt5UKIrj2e4A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.27.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.5.tgz",
+ "integrity": "sha512-hVRQX4+P3MS36NxOy24v/Cdsimy/5HYePw+tmPqnNN1fxV0bPrFWR6TMqwXPwoTM2VzbkA+4lbHWUKDd5ZDA/w==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.27.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.5.tgz",
+ "integrity": "sha512-mKqqRuOPALI8nDzhOBmIS0INvZOOFGGg5n1osGIXAx8oersceEbKd4t1ACNTHM3sJBXGFAlEgqM+svzjPot+ZQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.27.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.5.tgz",
+ "integrity": "sha512-EE/QXH9IyaAj1qeuIV5+/GZkBTipgGO782Ff7Um3vPS9cvLhJJeATy4Ggxikz2inZ46KByamMn6GqtqyVjhenA==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.27.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.5.tgz",
+ "integrity": "sha512-0V2iF1RGxBf1b7/BjurA5jfkl7PtySjom1r6xOK2q9KWw/XCpAdtB6KNMO+9xx69yYfSCRR9FE0TyKfHA2eQMw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.27.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.5.tgz",
+ "integrity": "sha512-rYxThBx6G9HN6tFNuvB/vykeLi4VDsm5hE5pVwzqbAjZEARQrWu3noZSfbEnPZ/CRXP3271GyFk/49up2W190g==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.27.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.5.tgz",
+ "integrity": "sha512-uEP2q/4qgd8goEUc4QIdU/1P2NmEtZ/zX5u3OpLlCGhJIuBIv0s0wr7TB2nBrd3/A5XIdEkkS5ZLF0ULuvaaYQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.27.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.5.tgz",
+ "integrity": "sha512-+Gq47Wqq6PLOOZuBzVSII2//9yyHNKZLuwfzCemqexqOQCSz0zy0O26kIzyp9EMNMK+nZ0tFHBZrCeVUuMs/ew==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.27.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.5.tgz",
+ "integrity": "sha512-3F/5EG8VHfN/I+W5cO1/SV2H9Q/5r7vcHabMnBqhHK2lTWOh3F8vixNzo8lqxrlmBtZVFpW8pmITHnq54+Tq4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.27.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.5.tgz",
+ "integrity": "sha512-28t+Sj3CPN8vkMOlZotOmDgilQwVvxWZl7b8rxpn73Tt/gCnvrHxQUMng4uu3itdFvrtba/1nHejvxqz8xgEMA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.27.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.5.tgz",
+ "integrity": "sha512-Doz/hKtiuVAi9hMsBMpwBANhIZc8l238U2Onko3t2xUp8xtM0ZKdDYHMnm/qPFVthY8KtxkXaocwmMh6VolzMA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.27.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.5.tgz",
+ "integrity": "sha512-WfGVaa1oz5A7+ZFPkERIbIhKT4olvGl1tyzTRaB5yoZRLqC0KwaO95FeZtOdQj/oKkjW57KcVF944m62/0GYtA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.27.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.5.tgz",
+ "integrity": "sha512-Xh+VRuh6OMh3uJ0JkCjI57l+DVe7VRGBYymen8rFPnTVgATBwA6nmToxM2OwTlSvrnWpPKkrQUj93+K9huYC6A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.27.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.5.tgz",
+ "integrity": "sha512-aC1gpJkkaUADHuAdQfuVTnqVUTLqqUNhAvEwHwVWcnVVZvNlDPGA0UveZsfXJJ9T6k9Po4eHi3c02gbdwO3g6w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.27.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.5.tgz",
+ "integrity": "sha512-0UNx2aavV0fk6UpZcwXFLztA2r/k9jTUa7OW7SAea1VYUhkug99MW1uZeXEnPn5+cHOd0n8myQay6TlFnBR07w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.27.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.5.tgz",
+ "integrity": "sha512-5nlJ3AeJWCTSzR7AEqVjT/faWyqKU86kCi1lLmxVqmNR+j4HrYdns+eTGjS/vmrzCIe8inGQckUadvS0+JkKdQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.27.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.5.tgz",
+ "integrity": "sha512-PWypQR+d4FLfkhBIV+/kHsUELAnMpx1bRvvsn3p+/sAERbnCzFrtDRG2Xw5n+2zPxBK2+iaP+vetsRl4Ti7WgA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz",
+ "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.122.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz",
+ "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@pyyush/dbar": {
+ "resolved": "../..",
+ "link": true
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz",
+ "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz",
+ "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz",
+ "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz",
+ "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz",
+ "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz",
+ "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz",
+ "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz",
+ "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz",
+ "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz",
+ "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz",
+ "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz",
+ "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz",
+ "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@napi-rs/wasm-runtime": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz",
+ "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz",
+ "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz",
+ "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
+ "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "22.19.15",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
+ "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/node-fetch": {
+ "version": "2.6.13",
+ "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz",
+ "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "form-data": "^4.0.4"
+ }
+ },
+ "node_modules/@vitest/expect": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz",
+ "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.1.0",
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "4.1.2",
+ "@vitest/utils": "4.1.2",
+ "chai": "^6.2.2",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz",
+ "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "4.1.2",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.21"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz",
+ "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz",
+ "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "4.1.2",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz",
+ "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.2",
+ "@vitest/utils": "4.1.2",
+ "magic-string": "^0.30.21",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz",
+ "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz",
+ "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.2",
+ "convert-source-map": "^2.0.0",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/abort-controller": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
+ "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
+ "license": "MIT",
+ "dependencies": {
+ "event-target-shim": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=6.5"
+ }
+ },
+ "node_modules/agentkeepalive": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
+ "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
+ "license": "MIT",
+ "dependencies": {
+ "humanize-ms": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 8.0.0"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/chai": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
+ "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
+ "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.27.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.5.tgz",
+ "integrity": "sha512-zdQoHBjuDqKsvV5OPaWansOwfSQ0Js+Uj9J85TBvj3bFW1JjWTSULMRwdQAc8qMeIScbClxeMK0jlrtB9linhA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.27.5",
+ "@esbuild/android-arm": "0.27.5",
+ "@esbuild/android-arm64": "0.27.5",
+ "@esbuild/android-x64": "0.27.5",
+ "@esbuild/darwin-arm64": "0.27.5",
+ "@esbuild/darwin-x64": "0.27.5",
+ "@esbuild/freebsd-arm64": "0.27.5",
+ "@esbuild/freebsd-x64": "0.27.5",
+ "@esbuild/linux-arm": "0.27.5",
+ "@esbuild/linux-arm64": "0.27.5",
+ "@esbuild/linux-ia32": "0.27.5",
+ "@esbuild/linux-loong64": "0.27.5",
+ "@esbuild/linux-mips64el": "0.27.5",
+ "@esbuild/linux-ppc64": "0.27.5",
+ "@esbuild/linux-riscv64": "0.27.5",
+ "@esbuild/linux-s390x": "0.27.5",
+ "@esbuild/linux-x64": "0.27.5",
+ "@esbuild/netbsd-arm64": "0.27.5",
+ "@esbuild/netbsd-x64": "0.27.5",
+ "@esbuild/openbsd-arm64": "0.27.5",
+ "@esbuild/openbsd-x64": "0.27.5",
+ "@esbuild/openharmony-arm64": "0.27.5",
+ "@esbuild/sunos-x64": "0.27.5",
+ "@esbuild/win32-arm64": "0.27.5",
+ "@esbuild/win32-ia32": "0.27.5",
+ "@esbuild/win32-x64": "0.27.5"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/event-target-shim": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
+ "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
+ "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/form-data-encoder": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
+ "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==",
+ "license": "MIT"
+ },
+ "node_modules/formdata-node": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
+ "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
+ "license": "MIT",
+ "dependencies": {
+ "node-domexception": "1.0.0",
+ "web-streams-polyfill": "4.0.0-beta.3"
+ },
+ "engines": {
+ "node": ">= 12.20"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/get-tsconfig": {
+ "version": "4.13.7",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz",
+ "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/humanize-ms": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
+ "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.0.0"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-domexception": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
+ "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
+ "deprecated": "Use your platform's native DOMException instead",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "github",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.5.0"
+ }
+ },
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/obug": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
+ "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT"
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.58.2",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
+ "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.8",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
+ "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz",
+ "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.122.0",
+ "@rolldown/pluginutils": "1.0.0-rc.12"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.0.0-rc.12",
+ "@rolldown/binding-darwin-arm64": "1.0.0-rc.12",
+ "@rolldown/binding-darwin-x64": "1.0.0-rc.12",
+ "@rolldown/binding-freebsd-x64": "1.0.0-rc.12",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12",
+ "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12",
+ "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12",
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12",
+ "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12",
+ "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12",
+ "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12",
+ "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12",
+ "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12",
+ "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12",
+ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12"
+ }
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/std-env": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz",
+ "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz",
+ "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
+ "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "license": "MIT"
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/tsx": {
+ "version": "4.21.0",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
+ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.27.0",
+ "get-tsconfig": "^4.7.5"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "license": "MIT"
+ },
+ "node_modules/vite": {
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz",
+ "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.4",
+ "postcss": "^8.5.8",
+ "rolldown": "1.0.0-rc.12",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.1.0",
+ "esbuild": "^0.27.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz",
+ "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "4.1.2",
+ "@vitest/mocker": "4.1.2",
+ "@vitest/pretty-format": "4.1.2",
+ "@vitest/runner": "4.1.2",
+ "@vitest/snapshot": "4.1.2",
+ "@vitest/spy": "4.1.2",
+ "@vitest/utils": "4.1.2",
+ "es-module-lexer": "^2.0.0",
+ "expect-type": "^1.3.0",
+ "magic-string": "^0.30.21",
+ "obug": "^2.1.1",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.3",
+ "std-env": "^4.0.0-rc.1",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^1.0.2",
+ "tinyglobby": "^0.2.15",
+ "tinyrainbow": "^3.1.0",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "4.1.2",
+ "@vitest/browser-preview": "4.1.2",
+ "@vitest/browser-webdriverio": "4.1.2",
+ "@vitest/ui": "4.1.2",
+ "happy-dom": "*",
+ "jsdom": "*",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ },
+ "vite": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/web-streams-polyfill": {
+ "version": "4.0.0-beta.3",
+ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
+ "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ }
+ }
+}
diff --git a/integrations/browserbase/package.json b/integrations/browserbase/package.json
new file mode 100644
index 0000000..a052323
--- /dev/null
+++ b/integrations/browserbase/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "@pyyush/dbar-browserbase",
+ "version": "0.2.0",
+ "description": "DBAR + Browserbase: deterministic capture in the cloud, replay locally",
+ "private": true,
+ "author": "Piyush Vyas",
+ "license": "Apache-2.0",
+ "type": "module",
+ "scripts": {
+ "capture": "npx tsx capture.ts",
+ "replay": "npx tsx replay.ts",
+ "example": "npx tsx example.ts",
+ "prepublishOnly": "node -e \"console.error('This integration package is intentionally not publishable.'); process.exit(1)\"",
+ "test": "vitest run --config vitest.config.ts"
+ },
+ "dependencies": {
+ "@pyyush/dbar": "file:../..",
+ "@browserbasehq/sdk": "2.9.0"
+ },
+ "peerDependencies": {
+ "playwright-core": ">=1.40.0"
+ },
+ "devDependencies": {
+ "@types/node": "22.19.15",
+ "playwright-core": "1.58.2",
+ "tsx": "4.21.0",
+ "typescript": "5.9.3",
+ "vitest": "4.1.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+}
diff --git a/integrations/browserbase/replay.ts b/integrations/browserbase/replay.ts
new file mode 100644
index 0000000..448582c
--- /dev/null
+++ b/integrations/browserbase/replay.ts
@@ -0,0 +1,104 @@
+/**
+ * DBAR Local Replay for Browserbase Capsules
+ *
+ * Replays a capsule recorded via Browserbase on a LOCAL browser.
+ * This is the value prop: "record in cloud, verify locally."
+ *
+ * No Browserbase credentials or connection needed — replay is fully local.
+ *
+ * Usage:
+ * npx tsx replay.ts [--json]
+ *
+ * Exit codes:
+ * 0 = replay succeeded (all steps matched)
+ * 1 = replay completed with divergences
+ * 2 = fatal error (missing file, bad capsule, etc.)
+ *
+ * @module
+ */
+
+import { readFileSync } from "node:fs";
+import { resolve } from "node:path";
+import { chromium } from "playwright-core";
+import { DBAR, deserializeCapsuleArchive } from "@pyyush/dbar";
+
+import { parseReplayArgs } from "./helpers.js";
+
+async function main(): Promise {
+ const args = parseReplayArgs(process.argv.slice(2));
+ if (args.error) {
+ console.error(`[dbar-replay] Error: ${args.error}`);
+ console.error("[dbar-replay] Usage: npx tsx replay.ts [--json]");
+ process.exit(2);
+ }
+
+ const capsulePath = resolve(args.capsulePath!);
+ console.error(`[dbar-replay] Loading capsule from: ${capsulePath}`);
+
+ let serialized: string;
+ try {
+ serialized = readFileSync(capsulePath, "utf-8");
+ } catch (err: unknown) {
+ const message = err instanceof Error ? err.message : String(err);
+ console.error(`[dbar-replay] Failed to read capsule file: ${message}`);
+ process.exit(2);
+ }
+
+ const archive = deserializeCapsuleArchive(serialized);
+ const manifest = archive.manifest;
+
+ console.error(`[dbar-replay] Capsule ID: ${manifest.id}`);
+ console.error(
+ `[dbar-replay] Steps: ${manifest.steps.length}, ` +
+ `Requests: ${manifest.networkTranscript.entries.length}`
+ );
+
+ console.error("[dbar-replay] Launching local browser for replay...");
+ const noSandbox = process.env["DBAR_NO_SANDBOX"] === "1";
+ const browser = await chromium.launch({
+ headless: true,
+ args: [
+ "--disable-gpu",
+ ...(noSandbox ? ["--no-sandbox"] : []),
+ ],
+ });
+
+ const context = await browser.newContext({
+ viewport: {
+ width: manifest.environment.viewport.width,
+ height: manifest.environment.viewport.height,
+ },
+ locale: manifest.environment.locale,
+ timezoneId: manifest.environment.timezone,
+ userAgent: manifest.environment.userAgent,
+ });
+
+ const page = await context.newPage();
+
+ console.error("[dbar-replay] Starting replay...");
+ const result = await DBAR.replay(page, archive);
+
+ if (args.json) {
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
+ } else {
+ console.log(`[dbar-replay] Replay complete.`);
+ console.log(`[dbar-replay] Success: ${result.success}`);
+ console.log(`[dbar-replay] RSR: ${(result.replaySuccessRate * 100).toFixed(1)}%`);
+ console.log(`[dbar-replay] DVR: ${(result.determinismViolationRate * 100).toFixed(1)}%`);
+ console.log(`[dbar-replay] Divergences: ${result.divergences.length}`);
+ console.log(`[dbar-replay] Overhead: ${result.overheadMs}ms`);
+
+ if (result.timeToDivergence !== undefined) {
+ console.log(`[dbar-replay] First divergence at step: ${result.timeToDivergence}`);
+ }
+ }
+
+ await browser.close();
+
+ process.exit(result.success ? 0 : 1);
+}
+
+main().catch((error: unknown) => {
+ console.error("[dbar-replay] Fatal error:", error);
+ process.exit(2);
+});
diff --git a/integrations/browserbase/tsconfig.json b/integrations/browserbase/tsconfig.json
new file mode 100644
index 0000000..7ec13e3
--- /dev/null
+++ b/integrations/browserbase/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "lib": ["ES2022"],
+ "outDir": "./dist",
+ "declaration": true,
+ "sourceMap": true,
+ "strict": true,
+ "noImplicitAny": true,
+ "strictNullChecks": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedIndexedAccess": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true
+ },
+ "include": ["*.ts"],
+ "exclude": ["node_modules", "dist", "__tests__"]
+}
diff --git a/integrations/browserbase/vitest.config.ts b/integrations/browserbase/vitest.config.ts
new file mode 100644
index 0000000..8c3ee95
--- /dev/null
+++ b/integrations/browserbase/vitest.config.ts
@@ -0,0 +1,9 @@
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ test: {
+ globals: true,
+ environment: "node",
+ include: ["__tests__/**/*.test.ts"],
+ },
+});
diff --git a/package-lock.json b/package-lock.json
index c6d952d..3859ffd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,17 +1,21 @@
{
- "name": "dbar",
- "version": "0.1.0",
+ "name": "@pyyush/dbar",
+ "version": "0.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "dbar",
- "version": "0.1.0",
+ "name": "@pyyush/dbar",
+ "version": "0.2.0",
"license": "Apache-2.0",
"dependencies": {
"zod": "^3.23.0"
},
+ "bin": {
+ "dbar": "dist/cli.js"
+ },
"devDependencies": {
+ "@changesets/cli": "^2.30.0",
"@types/node": "^25.5.0",
"playwright-core": "^1.52.0",
"tsup": "^8.3.0",
@@ -30,6 +34,258 @@
}
}
},
+ "node_modules/@babel/runtime": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
+ "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@changesets/apply-release-plan": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-7.1.0.tgz",
+ "integrity": "sha512-yq8ML3YS7koKQ/9bk1PqO0HMzApIFNwjlwCnwFEXMzNe8NpzeeYYKCmnhWJGkN8g7E51MnWaSbqRcTcdIxUgnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@changesets/config": "^3.1.3",
+ "@changesets/get-version-range-type": "^0.4.0",
+ "@changesets/git": "^3.0.4",
+ "@changesets/should-skip-package": "^0.1.2",
+ "@changesets/types": "^6.1.0",
+ "@manypkg/get-packages": "^1.1.3",
+ "detect-indent": "^6.0.0",
+ "fs-extra": "^7.0.1",
+ "lodash.startcase": "^4.4.0",
+ "outdent": "^0.5.0",
+ "prettier": "^2.7.1",
+ "resolve-from": "^5.0.0",
+ "semver": "^7.5.3"
+ }
+ },
+ "node_modules/@changesets/assemble-release-plan": {
+ "version": "6.0.9",
+ "resolved": "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-6.0.9.tgz",
+ "integrity": "sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@changesets/errors": "^0.2.0",
+ "@changesets/get-dependents-graph": "^2.1.3",
+ "@changesets/should-skip-package": "^0.1.2",
+ "@changesets/types": "^6.1.0",
+ "@manypkg/get-packages": "^1.1.3",
+ "semver": "^7.5.3"
+ }
+ },
+ "node_modules/@changesets/changelog-git": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/@changesets/changelog-git/-/changelog-git-0.2.1.tgz",
+ "integrity": "sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@changesets/types": "^6.1.0"
+ }
+ },
+ "node_modules/@changesets/cli": {
+ "version": "2.30.0",
+ "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.30.0.tgz",
+ "integrity": "sha512-5D3Nk2JPqMI1wK25pEymeWRSlSMdo5QOGlyfrKg0AOufrUcjEE3RQgaCpHoBiM31CSNrtSgdJ0U6zL1rLDDfBA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@changesets/apply-release-plan": "^7.1.0",
+ "@changesets/assemble-release-plan": "^6.0.9",
+ "@changesets/changelog-git": "^0.2.1",
+ "@changesets/config": "^3.1.3",
+ "@changesets/errors": "^0.2.0",
+ "@changesets/get-dependents-graph": "^2.1.3",
+ "@changesets/get-release-plan": "^4.0.15",
+ "@changesets/git": "^3.0.4",
+ "@changesets/logger": "^0.1.1",
+ "@changesets/pre": "^2.0.2",
+ "@changesets/read": "^0.6.7",
+ "@changesets/should-skip-package": "^0.1.2",
+ "@changesets/types": "^6.1.0",
+ "@changesets/write": "^0.4.0",
+ "@inquirer/external-editor": "^1.0.2",
+ "@manypkg/get-packages": "^1.1.3",
+ "ansi-colors": "^4.1.3",
+ "enquirer": "^2.4.1",
+ "fs-extra": "^7.0.1",
+ "mri": "^1.2.0",
+ "package-manager-detector": "^0.2.0",
+ "picocolors": "^1.1.0",
+ "resolve-from": "^5.0.0",
+ "semver": "^7.5.3",
+ "spawndamnit": "^3.0.1",
+ "term-size": "^2.1.0"
+ },
+ "bin": {
+ "changeset": "bin.js"
+ }
+ },
+ "node_modules/@changesets/config": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@changesets/config/-/config-3.1.3.tgz",
+ "integrity": "sha512-vnXjcey8YgBn2L1OPWd3ORs0bGC4LoYcK/ubpgvzNVr53JXV5GiTVj7fWdMRsoKUH7hhhMAQnsJUqLr21EncNw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@changesets/errors": "^0.2.0",
+ "@changesets/get-dependents-graph": "^2.1.3",
+ "@changesets/logger": "^0.1.1",
+ "@changesets/should-skip-package": "^0.1.2",
+ "@changesets/types": "^6.1.0",
+ "@manypkg/get-packages": "^1.1.3",
+ "fs-extra": "^7.0.1",
+ "micromatch": "^4.0.8"
+ }
+ },
+ "node_modules/@changesets/errors": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/@changesets/errors/-/errors-0.2.0.tgz",
+ "integrity": "sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "extendable-error": "^0.1.5"
+ }
+ },
+ "node_modules/@changesets/get-dependents-graph": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-2.1.3.tgz",
+ "integrity": "sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@changesets/types": "^6.1.0",
+ "@manypkg/get-packages": "^1.1.3",
+ "picocolors": "^1.1.0",
+ "semver": "^7.5.3"
+ }
+ },
+ "node_modules/@changesets/get-release-plan": {
+ "version": "4.0.15",
+ "resolved": "https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-4.0.15.tgz",
+ "integrity": "sha512-Q04ZaRPuEVZtA+auOYgFaVQQSA98dXiVe/yFaZfY7hoSmQICHGvP0TF4u3EDNHWmmCS4ekA/XSpKlSM2PyTS2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@changesets/assemble-release-plan": "^6.0.9",
+ "@changesets/config": "^3.1.3",
+ "@changesets/pre": "^2.0.2",
+ "@changesets/read": "^0.6.7",
+ "@changesets/types": "^6.1.0",
+ "@manypkg/get-packages": "^1.1.3"
+ }
+ },
+ "node_modules/@changesets/get-version-range-type": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@changesets/get-version-range-type/-/get-version-range-type-0.4.0.tgz",
+ "integrity": "sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@changesets/git": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@changesets/git/-/git-3.0.4.tgz",
+ "integrity": "sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@changesets/errors": "^0.2.0",
+ "@manypkg/get-packages": "^1.1.3",
+ "is-subdir": "^1.1.1",
+ "micromatch": "^4.0.8",
+ "spawndamnit": "^3.0.1"
+ }
+ },
+ "node_modules/@changesets/logger": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@changesets/logger/-/logger-0.1.1.tgz",
+ "integrity": "sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picocolors": "^1.1.0"
+ }
+ },
+ "node_modules/@changesets/parse": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@changesets/parse/-/parse-0.4.3.tgz",
+ "integrity": "sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@changesets/types": "^6.1.0",
+ "js-yaml": "^4.1.1"
+ }
+ },
+ "node_modules/@changesets/pre": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@changesets/pre/-/pre-2.0.2.tgz",
+ "integrity": "sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@changesets/errors": "^0.2.0",
+ "@changesets/types": "^6.1.0",
+ "@manypkg/get-packages": "^1.1.3",
+ "fs-extra": "^7.0.1"
+ }
+ },
+ "node_modules/@changesets/read": {
+ "version": "0.6.7",
+ "resolved": "https://registry.npmjs.org/@changesets/read/-/read-0.6.7.tgz",
+ "integrity": "sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@changesets/git": "^3.0.4",
+ "@changesets/logger": "^0.1.1",
+ "@changesets/parse": "^0.4.3",
+ "@changesets/types": "^6.1.0",
+ "fs-extra": "^7.0.1",
+ "p-filter": "^2.1.0",
+ "picocolors": "^1.1.0"
+ }
+ },
+ "node_modules/@changesets/should-skip-package": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/@changesets/should-skip-package/-/should-skip-package-0.1.2.tgz",
+ "integrity": "sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@changesets/types": "^6.1.0",
+ "@manypkg/get-packages": "^1.1.3"
+ }
+ },
+ "node_modules/@changesets/types": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/@changesets/types/-/types-6.1.0.tgz",
+ "integrity": "sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@changesets/write": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@changesets/write/-/write-0.4.0.tgz",
+ "integrity": "sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@changesets/types": "^6.1.0",
+ "fs-extra": "^7.0.1",
+ "human-id": "^4.1.1",
+ "prettier": "^2.7.1"
+ }
+ },
"node_modules/@emnapi/core": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz",
@@ -506,6 +762,28 @@
"node": ">=18"
}
},
+ "node_modules/@inquirer/external-editor": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz",
+ "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chardet": "^2.1.1",
+ "iconv-lite": "^0.7.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -545,6 +823,78 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@manypkg/find-root": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@manypkg/find-root/-/find-root-1.1.0.tgz",
+ "integrity": "sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.5.5",
+ "@types/node": "^12.7.1",
+ "find-up": "^4.1.0",
+ "fs-extra": "^8.1.0"
+ }
+ },
+ "node_modules/@manypkg/find-root/node_modules/@types/node": {
+ "version": "12.20.55",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz",
+ "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@manypkg/find-root/node_modules/fs-extra": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
+ "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=6 <7 || >=8"
+ }
+ },
+ "node_modules/@manypkg/get-packages": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@manypkg/get-packages/-/get-packages-1.1.3.tgz",
+ "integrity": "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.5.5",
+ "@changesets/types": "^4.0.1",
+ "@manypkg/find-root": "^1.1.0",
+ "fs-extra": "^8.1.0",
+ "globby": "^11.0.0",
+ "read-yaml-file": "^1.1.0"
+ }
+ },
+ "node_modules/@manypkg/get-packages/node_modules/@changesets/types": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@changesets/types/-/types-4.1.0.tgz",
+ "integrity": "sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@manypkg/get-packages/node_modules/fs-extra": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
+ "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=6 <7 || >=8"
+ }
+ },
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
@@ -562,6 +912,44 @@
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
"node_modules/@oxc-project/types": {
"version": "0.122.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz",
@@ -1420,6 +1808,26 @@
"node": ">=0.4.0"
}
},
+ "node_modules/ansi-colors": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
+ "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/any-promise": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
@@ -1427,6 +1835,23 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/array-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/assertion-error": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
@@ -1437,6 +1862,32 @@
"node": ">=12"
}
},
+ "node_modules/better-path-resolve": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/better-path-resolve/-/better-path-resolve-1.0.0.tgz",
+ "integrity": "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-windows": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/bundle-require": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz",
@@ -1473,6 +1924,13 @@
"node": ">=18"
}
},
+ "node_modules/chardet": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz",
+ "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@@ -1523,6 +1981,21 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -1541,6 +2014,16 @@
}
}
},
+ "node_modules/detect-indent": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz",
+ "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -1551,6 +2034,33 @@
"node": ">=8"
}
},
+ "node_modules/dir-glob": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-type": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/enquirer": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz",
+ "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-colors": "^4.1.1",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
"node_modules/es-module-lexer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
@@ -1600,6 +2110,20 @@
"@esbuild/win32-x64": "0.27.4"
}
},
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
@@ -1620,6 +2144,40 @@
"node": ">=12.0.0"
}
},
+ "node_modules/extendable-error": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/extendable-error/-/extendable-error-0.1.7.tgz",
+ "integrity": "sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fastq": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
+ "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -1638,18 +2196,60 @@
}
}
},
- "node_modules/fix-dts-default-cjs-exports": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz",
- "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==",
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "magic-string": "^0.30.17",
- "mlly": "^1.7.4",
- "rollup": "^4.34.8"
- }
- },
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fix-dts-default-cjs-exports": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz",
+ "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "magic-string": "^0.30.17",
+ "mlly": "^1.7.4",
+ "rollup": "^4.34.8"
+ }
+ },
+ "node_modules/fs-extra": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
+ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.1.2",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=6 <7 || >=8"
+ }
+ },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1665,6 +2265,147 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/globby": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+ "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.2.9",
+ "ignore": "^5.2.0",
+ "merge2": "^1.4.1",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/human-id": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/human-id/-/human-id-4.1.3.tgz",
+ "integrity": "sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "human-id": "dist/cli.js"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-subdir": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/is-subdir/-/is-subdir-1.2.0.tgz",
+ "integrity": "sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "better-path-resolve": "1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/is-windows": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
+ "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/joycon": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
@@ -1675,6 +2416,29 @@
"node": ">=10"
}
},
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsonfile": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
+ "dev": true,
+ "license": "MIT",
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
"node_modules/lightningcss": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
@@ -1978,6 +2742,26 @@
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
+ "node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/lodash.startcase": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz",
+ "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -1988,6 +2772,43 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/micromatch/node_modules/picomatch": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/mlly": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz",
@@ -2001,6 +2822,16 @@
"ufo": "^1.6.3"
}
},
+ "node_modules/mri": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
+ "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -2060,6 +2891,115 @@
],
"license": "MIT"
},
+ "node_modules/outdent": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.5.0.tgz",
+ "integrity": "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/p-filter": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-2.1.0.tgz",
+ "integrity": "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-map": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/p-map": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz",
+ "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/package-manager-detector": {
+ "version": "0.2.11",
+ "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.11.tgz",
+ "integrity": "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "quansync": "^0.2.7"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
@@ -2087,6 +3027,16 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/pify": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
+ "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/pirates": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
@@ -2194,6 +3144,100 @@
}
}
},
+ "node_modules/prettier": {
+ "version": "2.8.8",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
+ "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin-prettier.js"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/quansync": {
+ "version": "0.2.11",
+ "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
+ "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/antfu"
+ },
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/sxzz"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/read-yaml-file": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/read-yaml-file/-/read-yaml-file-1.1.0.tgz",
+ "integrity": "sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.1.5",
+ "js-yaml": "^3.6.1",
+ "pify": "^4.0.1",
+ "strip-bom": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/read-yaml-file/node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/read-yaml-file/node_modules/js-yaml": {
+ "version": "3.14.2",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
+ "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
@@ -2218,6 +3262,17 @@
"node": ">=8"
}
},
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/rolldown": {
"version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.11.tgz",
@@ -2297,6 +3352,73 @@
"fsevents": "~2.3.2"
}
},
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/siginfo": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
@@ -2304,6 +3426,29 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/source-map": {
"version": "0.7.6",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
@@ -2324,6 +3469,24 @@
"node": ">=0.10.0"
}
},
+ "node_modules/spawndamnit": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/spawndamnit/-/spawndamnit-3.0.1.tgz",
+ "integrity": "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==",
+ "dev": true,
+ "license": "SEE LICENSE IN LICENSE",
+ "dependencies": {
+ "cross-spawn": "^7.0.5",
+ "signal-exit": "^4.0.1"
+ }
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
"node_modules/stackback": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
@@ -2338,6 +3501,29 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/sucrase": {
"version": "3.35.1",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
@@ -2361,6 +3547,19 @@
"node": ">=16 || 14 >=14.17"
}
},
+ "node_modules/term-size": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz",
+ "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/thenify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
@@ -2425,6 +3624,19 @@
"node": ">=14.0.0"
}
},
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
"node_modules/tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
@@ -2531,6 +3743,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/universalify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
"node_modules/vite": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.2.tgz",
@@ -2701,6 +3923,22 @@
"node": ">=18"
}
},
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
"node_modules/why-is-node-running": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
diff --git a/package.json b/package.json
index 3448111..a7a017b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@pyyush/dbar",
- "version": "0.1.0",
+ "version": "0.2.0",
"description": "DBAR — Deterministic Browser Agent Runtime. Replayable, verifiable browser executions.",
"author": "Piyush Vyas",
"license": "Apache-2.0",
@@ -21,6 +21,9 @@
},
"./package.json": "./package.json"
},
+ "bin": {
+ "dbar": "./dist/cli.js"
+ },
"files": [
"dist",
"README.md",
@@ -43,11 +46,15 @@
],
"scripts": {
"build": "tsup",
+ "changeset": "changeset",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest",
"lint": "eslint src",
"lint:fix": "eslint src --fix",
+ "release:verify": "npm run build && npm run typecheck && npm run test && npm pack --dry-run >/dev/null",
+ "release:version": "changeset version && node ./scripts/sync-python-version.mjs",
+ "release:publish:npm": "node ./scripts/publish-npm-if-needed.mjs",
"clean": "rm -rf dist",
"prepublishOnly": "npm run build"
},
@@ -63,6 +70,7 @@
}
},
"devDependencies": {
+ "@changesets/cli": "^2.30.0",
"@types/node": "^25.5.0",
"playwright-core": "^1.52.0",
"tsup": "^8.3.0",
diff --git a/python/README.md b/python/README.md
new file mode 100644
index 0000000..79a5383
--- /dev/null
+++ b/python/README.md
@@ -0,0 +1,148 @@
+# DBAR Python SDK
+
+**Open-source evidence capsules for `browser-use` runs.**
+
+The PyPI package records browser-use agent executions into shareable capsule files you can inspect, diff, and keep as regression artifacts.
+
+Use it when you need to answer:
+
+- What did the agent actually do?
+- What changed between two runs?
+- Can I keep this failure as evidence instead of re-debugging from scratch?
+
+## What The Python Package Does Today
+
+The current Python SDK is a **recorder and diff tool** for `browser-use`-style workflows.
+
+It can:
+
+- record per-step metadata from browser-use runs
+- capture page-state and screenshot hashes
+- record actions and optional thinking
+- redact sensitive URL query parameters
+- write a capsule manifest to disk
+- diff two recorded runs step by step
+
+It does **not** yet provide the full deterministic replay engine from the TypeScript package. If you need deterministic capture and replay with CDP-level time/network control, use the main package in the repo root.
+
+## Version Compatibility
+
+- DBAR Python releases track the repo release line
+- `browser-use 0.12.5`
+- Python 3.11+ for the `browser-use` extra
+
+## Install
+
+```bash
+pip install dbar
+```
+
+For browser-use integration:
+
+```bash
+pip install dbar[browser-use]
+```
+
+The `browser-use` extra is pinned to `browser-use==0.12.5` and requires
+Python 3.11 or newer because that upstream package does.
+
+If you only need capsule loading and diffing, `pip install dbar` is enough.
+If you want the browser-use hook integration, install the extra.
+
+## Quick Start
+
+```python
+from browser_use import Agent
+from dbar import DBARRecorder
+
+recorder = DBARRecorder(output_dir="./capsules")
+agent = Agent(task="...")
+
+# Pass recorder.on_step_end to agent.run(...)
+await agent.run(on_step_end=recorder.on_step_end)
+
+capsule = recorder.finish()
+print(capsule.summary())
+```
+
+That writes a `capsule.json` manifest you can keep, inspect, or diff against later runs.
+
+Under browser-use 0.12.5, the recorder prefers the live
+`agent.browser_session.get_browser_state_summary(...)` surface during the hook.
+That gives DBAR a current page-state fingerprint and screenshot when available,
+instead of relying only on the persisted history shape.
+
+## Why Use It
+
+- **Proof**: keep a durable record of what the agent did
+- **Diffing**: compare two runs without manually inspecting every step
+- **Regression artifacts**: keep failed runs around as evidence
+- **Low friction**: add one recorder and one hook to an existing browser-use flow
+
+## Compare Two Runs
+
+```python
+from dbar import Capsule
+
+a = Capsule.load("./capsules/run1/capsule.json")
+b = Capsule.load("./capsules/run2/capsule.json")
+
+divergences = a.diff(b)
+for d in divergences:
+ print(f"Step {d['step']}: {d['field']} diverged")
+```
+
+## Configuration
+
+| Option | Type | Default | Description |
+|--------|------|---------|-------------|
+| `output_dir` | `str` | `"./dbar_output"` | Directory for capsule output |
+| `include_screenshots` | `bool` | `True` | Record screenshot hashes |
+| `include_dom` | `bool` | `True` | Record page-state hashes |
+| `include_actions` | `bool` | `True` | Record browser actions |
+| `include_thinking` | `bool` | `False` | Record model reasoning |
+| `redact_sensitive` | `bool` | `False` | Redact URL query params |
+
+## What A Capsule Contains
+
+The Python SDK currently writes a manifest with per-step information such as:
+
+- step index
+- URL
+- page-state hash
+- screenshot hash
+- action
+- optional thinking
+- timestamp
+
+This is enough to inspect and compare runs, even though it is not yet the full replay capsule format from the TypeScript engine.
+
+## When To Use Python vs TypeScript
+
+Use the **Python SDK** when:
+
+- your workflow is already built around `browser-use`
+- you want quick evidence capture with minimal integration work
+- you need run-to-run diffing more than deterministic replay
+
+Use the **TypeScript package** when:
+
+- you need deterministic replay
+- you need CDP-level time and network control
+- you want strict replay verification and divergence detection
+
+Install that package from npm:
+
+```bash
+npm install @pyyush/dbar playwright-core
+```
+
+## Open Source
+
+DBAR is being built as an open-source project.
+
+The goal is simple: if a browser workflow matters, it should emit a capsule you can keep, inspect, and trust.
+
+## License
+
+Apache-2.0
diff --git a/python/dbar/__init__.py b/python/dbar/__init__.py
new file mode 100644
index 0000000..62c0849
--- /dev/null
+++ b/python/dbar/__init__.py
@@ -0,0 +1,16 @@
+"""DBAR — Deterministic Browser Agent Runtime (Python SDK).
+
+Provides recording and comparison of browser-use agent executions
+via determinism capsules.
+
+Exports:
+ DBARRecorder: Records browser-use agent steps into a capsule.
+ Capsule: Loads, diffs, and summarizes recorded capsules.
+ __version__: Package version string.
+"""
+
+from dbar._version import __version__
+from dbar.capsule import Capsule
+from dbar.recorder import DBARRecorder
+
+__all__ = ["DBARRecorder", "Capsule", "__version__"]
diff --git a/python/dbar/_version.py b/python/dbar/_version.py
new file mode 100644
index 0000000..932e0f1
--- /dev/null
+++ b/python/dbar/_version.py
@@ -0,0 +1,3 @@
+"""Single-source version for the dbar package."""
+
+__version__ = "0.2.0"
diff --git a/python/dbar/capsule.py b/python/dbar/capsule.py
new file mode 100644
index 0000000..c664f51
--- /dev/null
+++ b/python/dbar/capsule.py
@@ -0,0 +1,137 @@
+"""Capsule loading, diffing, and summarization.
+
+A Capsule represents a recorded browser-use session stored as JSON.
+It supports step-by-step comparison of DOM and screenshot hashes
+to detect determinism divergences between runs.
+"""
+
+from __future__ import annotations
+
+import json
+import os
+from dataclasses import dataclass, field
+from typing import Any, Dict, List, Optional
+
+from dbar.types import CapsuleManifest
+
+
+@dataclass
+class Capsule:
+ """A loaded DBAR capsule with methods for comparison and inspection.
+
+ Attributes:
+ path: Absolute path to the capsule JSON file.
+ step_count: Number of steps in the capsule.
+ size_kb: File size in kilobytes.
+ manifest: The deserialized CapsuleManifest.
+
+ Example:
+ >>> capsule = Capsule.load("./capsules/run1/capsule.json")
+ >>> print(capsule.summary())
+ """
+
+ path: str
+ step_count: int
+ size_kb: float
+ manifest: CapsuleManifest
+
+ @classmethod
+ def load(cls, path: str) -> Capsule:
+ """Load a capsule from a JSON file on disk.
+
+ Args:
+ path: Path to the capsule JSON file.
+
+ Returns:
+ A Capsule instance populated from the file.
+
+ Raises:
+ FileNotFoundError: If *path* does not exist.
+ ValueError: If the file contains invalid JSON.
+ """
+ if not os.path.exists(path):
+ raise FileNotFoundError(f"Capsule file not found: {path}")
+
+ try:
+ with open(path, "r") as f:
+ data = json.load(f)
+ except json.JSONDecodeError as exc:
+ raise ValueError(
+ f"Failed to parse capsule JSON at {path}: {exc}"
+ ) from exc
+
+ manifest = CapsuleManifest.from_dict(data)
+ size_kb = os.path.getsize(path) / 1024
+
+ return cls(
+ path=path,
+ step_count=manifest.step_count,
+ size_kb=size_kb,
+ manifest=manifest,
+ )
+
+ def diff(self, other: Capsule) -> List[Dict[str, Any]]:
+ """Compare this capsule against another step by step.
+
+ Compares dom_hash and screenshot_hash for each overlapping step.
+ Reports a step_count_mismatch if the capsules have different lengths.
+
+ Args:
+ other: The capsule to compare against.
+
+ Returns:
+ A list of divergence dicts, each with keys: step, field, type,
+ expected, actual. Empty list if capsules are identical.
+ """
+ divergences: List[Dict[str, Any]] = []
+
+ self_steps = self.manifest.steps
+ other_steps = other.manifest.steps
+
+ if self.step_count != other.step_count:
+ divergences.append({
+ "type": "step_count_mismatch",
+ "step": -1,
+ "field": "step_count",
+ "expected": self.step_count,
+ "actual": other.step_count,
+ })
+
+ # Compare overlapping steps on hashable fields
+ comparable_fields = ["dom_hash", "screenshot_hash"]
+ min_steps = min(len(self_steps), len(other_steps))
+ for i in range(min_steps):
+ step_a = self_steps[i]
+ step_b = other_steps[i]
+ for hash_field in comparable_fields:
+ val_a = step_a.get(hash_field)
+ val_b = step_b.get(hash_field)
+ # Only compare when at least one side has a value
+ if val_a is None and val_b is None:
+ continue
+ if val_a != val_b:
+ divergences.append({
+ "type": "hash_mismatch",
+ "step": i,
+ "field": hash_field,
+ "expected": val_a,
+ "actual": val_b,
+ })
+
+ return divergences
+
+ def summary(self) -> str:
+ """Return a human-readable summary of this capsule.
+
+ Returns:
+ A multi-line string with path, version, step count, and size.
+ """
+ lines = [
+ f"DBAR Capsule v{self.manifest.version}",
+ f" Path: {self.path}",
+ f" Steps: {self.step_count}",
+ f" Size: {self.size_kb:.2f} KB",
+ ]
+ if self.manifest.created_at:
+ lines.append(f" Created: {self.manifest.created_at}")
+ return "\n".join(lines)
diff --git a/python/dbar/recorder.py b/python/dbar/recorder.py
new file mode 100644
index 0000000..87fa2ac
--- /dev/null
+++ b/python/dbar/recorder.py
@@ -0,0 +1,201 @@
+"""DBAR recorder for browser-use agent sessions.
+
+Captures per-step data (DOM, screenshots, actions, thinking) from a
+browser-use Agent, hashes content with SHA-256, and writes a capsule
+JSON file when finished.
+
+Uses ``from __future__ import annotations`` and TYPE_CHECKING to avoid
+importing browser-use at runtime — the recorder works with any object
+that has the expected ``history.history`` attribute shape.
+"""
+
+from __future__ import annotations
+
+import hashlib
+import json
+import os
+from datetime import datetime, timezone
+from typing import TYPE_CHECKING, Any, List, Optional
+from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
+
+from dbar.types import CapsuleManifest, StepSnapshot
+
+if TYPE_CHECKING:
+ pass
+
+
+class DBARRecorder:
+ """Records browser-use agent steps into a DBAR capsule.
+
+ Designed to be passed as the ``on_step_end`` hook to a browser-use Agent.
+ After the agent finishes, call :meth:`finish` to write the capsule JSON
+ and get a :class:`~dbar.capsule.Capsule` object.
+
+ Args:
+ output_dir: Directory where capsule.json will be written.
+ include_screenshots: Whether to hash and record screenshots.
+ include_dom: Whether to hash and record DOM snapshots.
+ include_actions: Whether to record browser actions.
+ include_thinking: Whether to record model reasoning/thinking.
+ redact_sensitive: Whether to redact URL query parameters.
+
+ Example:
+ >>> recorder = DBARRecorder(output_dir="./capsules")
+ >>> agent = Agent(task="...", on_step_end=recorder.on_step_end)
+ >>> await agent.run()
+ >>> capsule = recorder.finish()
+ """
+
+ def __init__(
+ self,
+ output_dir: str = "./dbar_output",
+ include_screenshots: bool = True,
+ include_dom: bool = True,
+ include_actions: bool = True,
+ include_thinking: bool = False,
+ redact_sensitive: bool = False,
+ ) -> None:
+ self._output_dir = output_dir
+ self._include_screenshots = include_screenshots
+ self._include_dom = include_dom
+ self._include_actions = include_actions
+ self._include_thinking = include_thinking
+ self._redact_sensitive = redact_sensitive
+ self._snapshots: List[StepSnapshot] = []
+ self._finished = False
+
+ async def on_step_end(self, agent: Any) -> None:
+ """Extract and record data from the agent's latest history step.
+
+ This method is designed to be passed as the ``on_step_end`` callback
+ to a browser-use Agent. It reads the last entry from
+ ``agent.history.history`` and captures configured fields.
+
+ Args:
+ agent: A browser-use Agent (or mock) with a
+ ``history.history`` list of step entries.
+ """
+ history_list = agent.history.history
+ if not history_list:
+ return
+
+ step = history_list[-1]
+ index = len(self._snapshots)
+
+ dom_hash: Optional[str] = None
+ screenshot_hash: Optional[str] = None
+ action: Optional[str] = None
+ thinking: Optional[str] = None
+ url: Optional[str] = None
+
+ # Extract state data
+ state = getattr(step, "state", None)
+ if state is not None:
+ url = getattr(state, "url", None)
+ if self._redact_sensitive and url:
+ url = self._redact_url(url)
+
+ if self._include_dom:
+ element_tree = getattr(state, "element_tree", None)
+ if element_tree is not None:
+ dom_text = element_tree.to_string()
+ dom_hash = hashlib.sha256(dom_text.encode("utf-8")).hexdigest()
+
+ if self._include_screenshots:
+ screenshot_data = getattr(state, "screenshot", None)
+ if screenshot_data is not None:
+ screenshot_hash = hashlib.sha256(
+ screenshot_data.encode("utf-8")
+ ).hexdigest()
+
+ # Extract action
+ if self._include_actions:
+ model_output = getattr(step, "model_output", None)
+ if model_output is not None:
+ action_list = getattr(model_output, "action", None)
+ if action_list:
+ action = str(action_list)
+
+ # Extract thinking
+ if self._include_thinking:
+ model_output = getattr(step, "model_output", None)
+ if model_output is not None:
+ current_state = getattr(model_output, "current_state", None)
+ if current_state is not None:
+ next_goal = getattr(current_state, "next_goal", None)
+ if next_goal:
+ thinking = next_goal
+
+ timestamp = datetime.now(timezone.utc).isoformat()
+
+ snapshot = StepSnapshot(
+ index=index,
+ dom_hash=dom_hash,
+ screenshot_hash=screenshot_hash,
+ action=action,
+ thinking=thinking,
+ url=url,
+ timestamp=timestamp,
+ )
+ self._snapshots.append(snapshot)
+
+ def finish(self) -> "Capsule":
+ """Write the capsule JSON and return a Capsule object.
+
+ Creates ``output_dir`` if it does not exist, writes ``capsule.json``,
+ and returns a loaded :class:`~dbar.capsule.Capsule`.
+
+ Returns:
+ A Capsule object representing the written file.
+
+ Raises:
+ RuntimeError: If ``finish()`` has already been called on this recorder.
+ """
+ # Lazy import to avoid circular dependency (capsule imports types,
+ # recorder imports types, __init__ imports both)
+ from dbar.capsule import Capsule
+
+ if self._finished:
+ raise RuntimeError(
+ "Recorder already finished — each DBARRecorder can only be "
+ "finished once. Create a new recorder for a new session."
+ )
+ self._finished = True
+
+ os.makedirs(self._output_dir, exist_ok=True)
+
+ manifest = CapsuleManifest(
+ version="0.1.0",
+ step_count=len(self._snapshots),
+ steps=[s.to_dict() for s in self._snapshots],
+ include_screenshots=self._include_screenshots,
+ include_dom=self._include_dom,
+ include_actions=self._include_actions,
+ include_thinking=self._include_thinking,
+ redact_sensitive=self._redact_sensitive,
+ created_at=datetime.now(timezone.utc).isoformat(),
+ )
+
+ capsule_path = os.path.join(self._output_dir, "capsule.json")
+ with open(capsule_path, "w") as f:
+ json.dump(manifest.to_dict(), f, indent=2)
+
+ return Capsule.load(capsule_path)
+
+ @staticmethod
+ def _redact_url(url: str) -> str:
+ """Replace all URL query parameter values with REDACTED.
+
+ Args:
+ url: The original URL string.
+
+ Returns:
+ The URL with all query parameter values replaced.
+ """
+ parsed = urlparse(url)
+ if not parsed.query:
+ return url
+ params = parse_qs(parsed.query, keep_blank_values=True)
+ redacted = {k: ["REDACTED"] for k in params}
+ new_query = urlencode(redacted, doseq=True)
+ return urlunparse(parsed._replace(query=new_query))
diff --git a/python/dbar/types.py b/python/dbar/types.py
new file mode 100644
index 0000000..4fd1e3a
--- /dev/null
+++ b/python/dbar/types.py
@@ -0,0 +1,147 @@
+"""Data types for DBAR step snapshots and capsule manifests.
+
+Uses dataclasses with to_dict/from_dict for JSON serialization without
+external dependencies. All types use ``from __future__ import annotations``
+for Python 3.9 compatibility.
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from typing import Any, Dict, List, Optional
+
+
+@dataclass
+class StepSnapshot:
+ """A single recorded step captured by the DBAR recorder.
+
+ Attributes:
+ index: Zero-based step number.
+ dom_hash: SHA-256 hash of the DOM state after this step, or None if DOM capture is disabled.
+ screenshot_hash: SHA-256 hash of the screenshot after this step, or None if screenshots are disabled.
+ action: The browser-use action taken at this step, or None.
+ thinking: The model's thinking/reasoning at this step, or None.
+ url: The page URL at this step, or None.
+ timestamp: ISO-8601 timestamp of capture.
+
+ Example:
+ >>> snap = StepSnapshot(index=0, dom_hash="abc123", url="https://example.com")
+ >>> snap.to_dict()["index"]
+ 0
+ """
+
+ index: int
+ dom_hash: Optional[str] = None
+ screenshot_hash: Optional[str] = None
+ action: Optional[str] = None
+ thinking: Optional[str] = None
+ url: Optional[str] = None
+ timestamp: Optional[str] = None
+
+ def to_dict(self) -> Dict[str, Any]:
+ """Serialize to a plain dictionary suitable for JSON encoding."""
+ result: Dict[str, Any] = {"index": self.index}
+ if self.dom_hash is not None:
+ result["dom_hash"] = self.dom_hash
+ if self.screenshot_hash is not None:
+ result["screenshot_hash"] = self.screenshot_hash
+ if self.action is not None:
+ result["action"] = self.action
+ if self.thinking is not None:
+ result["thinking"] = self.thinking
+ if self.url is not None:
+ result["url"] = self.url
+ if self.timestamp is not None:
+ result["timestamp"] = self.timestamp
+ return result
+
+ @classmethod
+ def from_dict(cls, data: Dict[str, Any]) -> StepSnapshot:
+ """Deserialize from a plain dictionary.
+
+ Args:
+ data: Dictionary with at minimum an ``index`` key.
+
+ Returns:
+ A new StepSnapshot instance.
+
+ Raises:
+ KeyError: If ``index`` is missing from *data*.
+ """
+ return cls(
+ index=data["index"],
+ dom_hash=data.get("dom_hash"),
+ screenshot_hash=data.get("screenshot_hash"),
+ action=data.get("action"),
+ thinking=data.get("thinking"),
+ url=data.get("url"),
+ timestamp=data.get("timestamp"),
+ )
+
+
+@dataclass
+class CapsuleManifest:
+ """Manifest describing a DBAR capsule: its metadata and recorded steps.
+
+ Attributes:
+ version: Capsule format version (currently "0.1.0").
+ step_count: Number of steps recorded.
+ steps: List of per-step snapshot dictionaries.
+ include_screenshots: Whether screenshots were captured.
+ include_dom: Whether DOM snapshots were captured.
+ include_actions: Whether actions were captured.
+ include_thinking: Whether model thinking was captured.
+ redact_sensitive: Whether sensitive data was redacted.
+ created_at: ISO-8601 creation timestamp.
+
+ Example:
+ >>> m = CapsuleManifest(version="0.1.0", step_count=2, steps=[])
+ >>> m.to_dict()["version"]
+ '0.1.0'
+ """
+
+ version: str = "0.1.0"
+ step_count: int = 0
+ steps: List[Dict[str, Any]] = field(default_factory=list)
+ include_screenshots: bool = True
+ include_dom: bool = True
+ include_actions: bool = True
+ include_thinking: bool = False
+ redact_sensitive: bool = False
+ created_at: Optional[str] = None
+
+ def to_dict(self) -> Dict[str, Any]:
+ """Serialize to a plain dictionary suitable for JSON encoding."""
+ return {
+ "version": self.version,
+ "step_count": self.step_count,
+ "steps": self.steps,
+ "include_screenshots": self.include_screenshots,
+ "include_dom": self.include_dom,
+ "include_actions": self.include_actions,
+ "include_thinking": self.include_thinking,
+ "redact_sensitive": self.redact_sensitive,
+ "created_at": self.created_at,
+ }
+
+ @classmethod
+ def from_dict(cls, data: Dict[str, Any]) -> CapsuleManifest:
+ """Deserialize from a plain dictionary.
+
+ Args:
+ data: Dictionary of manifest fields. All fields have defaults.
+
+ Returns:
+ A new CapsuleManifest instance.
+ """
+ return cls(
+ version=data.get("version", "0.1.0"),
+ step_count=data.get("step_count", 0),
+ steps=data.get("steps", []),
+ include_screenshots=data.get("include_screenshots", True),
+ include_dom=data.get("include_dom", True),
+ include_actions=data.get("include_actions", True),
+ include_thinking=data.get("include_thinking", False),
+ redact_sensitive=data.get("redact_sensitive", False),
+ created_at=data.get("created_at"),
+ )
diff --git a/python/pyproject.toml b/python/pyproject.toml
new file mode 100644
index 0000000..4da59bb
--- /dev/null
+++ b/python/pyproject.toml
@@ -0,0 +1,36 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "dbar"
+version = "0.2.0"
+description = "Evidence capsules for browser-use runs: record, diff, and keep browser agent traces"
+readme = "README.md"
+license = "Apache-2.0"
+requires-python = ">=3.9"
+authors = [
+ { name = "Piyush Vyas" },
+]
+classifiers = [
+ "Development Status :: 3 - Alpha",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: Apache Software License",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Topic :: Software Development :: Testing",
+]
+
+[project.optional-dependencies]
+browser-use = ["browser-use==0.12.5; python_version >= '3.11'"]
+dev = [
+ "pytest>=7.0",
+ "pytest-asyncio>=0.21",
+]
+
+[tool.pytest.ini_options]
+asyncio_mode = "auto"
+testpaths = ["tests"]
diff --git a/python/tests/__init__.py b/python/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/python/tests/conftest.py b/python/tests/conftest.py
new file mode 100644
index 0000000..8ed571b
--- /dev/null
+++ b/python/tests/conftest.py
@@ -0,0 +1,169 @@
+"""Mock browser-use objects for hermetic testing.
+
+These simple classes mimic the browser-use Agent, AgentHistory, and related
+types without importing browser-use. This keeps tests fast and dependency-free.
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from typing import Any, List, Optional
+
+import pytest
+
+
+@dataclass
+class MockActionResult:
+ """Mimics browser-use ActionResult."""
+
+ extracted_content: Optional[str] = None
+ error: Optional[str] = None
+ is_done: bool = False
+
+
+@dataclass
+class MockAgentOutput:
+ """Mimics browser-use AgentOutput (model response)."""
+
+ current_state: Optional[MockAgentState] = None
+ action: Optional[List[Any]] = None
+
+
+@dataclass
+class MockAgentState:
+ """Mimics the current_state field of AgentOutput."""
+
+ evaluation_previous_goal: str = ""
+ memory: str = ""
+ next_goal: str = ""
+
+
+@dataclass
+class MockBrowserStateHistory:
+ """Mimics browser-use BrowserStateHistory (page state at a step)."""
+
+ url: str = "https://example.com"
+ title: str = "Example"
+ tabs: List[Any] = field(default_factory=list)
+ screenshot: Optional[str] = None # base64 PNG
+ element_tree: Optional[MockDOMTree] = None
+
+
+@dataclass
+class MockDOMTree:
+ """Mimics a simplified DOM element tree."""
+
+ tag_name: str = "html"
+ text: str = ""
+ children: List[Any] = field(default_factory=list)
+
+ def to_string(self) -> str:
+ return f"<{self.tag_name}>{self.text}{self.tag_name}>"
+
+
+@dataclass
+class MockStepMetadata:
+ """Mimics browser-use step metadata."""
+
+ step_id: int = 0
+ step_start_time: float = 0.0
+ step_end_time: float = 1.0
+
+
+@dataclass
+class MockAgentHistory:
+ """Mimics a single entry in browser-use AgentHistory.history list."""
+
+ state: Optional[MockBrowserStateHistory] = None
+ model_output: Optional[MockAgentOutput] = None
+ result: Optional[List[MockActionResult]] = field(default_factory=list)
+ metadata: Optional[MockStepMetadata] = None
+
+
+@dataclass
+class MockHistoryList:
+ """Mimics the AgentHistory container that holds a list of history entries."""
+
+ history: List[MockAgentHistory] = field(default_factory=list)
+
+
+@dataclass
+class MockAgent:
+ """Mimics browser-use Agent with a history attribute."""
+
+ history: MockHistoryList = field(default_factory=MockHistoryList)
+
+
+def make_mock_agent(
+ url: str = "https://example.com",
+ title: str = "Example",
+ screenshot: Optional[str] = None,
+ dom_text: str = "hello",
+ action_text: Optional[str] = "click button",
+ thinking: Optional[str] = None,
+ num_steps: int = 1,
+) -> MockAgent:
+ """Create a MockAgent with pre-populated history steps.
+
+ Args:
+ url: Page URL for each step.
+ title: Page title for each step.
+ screenshot: Base64 screenshot string, or None.
+ dom_text: Raw DOM text for hashing.
+ action_text: Action description, or None.
+ thinking: Model thinking text, or None.
+ num_steps: Number of history steps to create.
+
+ Returns:
+ A MockAgent ready for use in recorder tests.
+ """
+ agent = MockAgent()
+ for i in range(num_steps):
+ dom_tree = MockDOMTree(text=dom_text)
+ state = MockBrowserStateHistory(
+ url=url,
+ title=title,
+ screenshot=screenshot,
+ element_tree=dom_tree,
+ )
+ model_output = None
+ if action_text or thinking:
+ agent_state = MockAgentState(next_goal=thinking or "")
+ model_output = MockAgentOutput(
+ current_state=agent_state,
+ action=[{"action": action_text}] if action_text else None,
+ )
+ result = [MockActionResult(extracted_content=action_text)]
+ metadata = MockStepMetadata(step_id=i)
+ entry = MockAgentHistory(
+ state=state,
+ model_output=model_output,
+ result=result,
+ metadata=metadata,
+ )
+ agent.history.history.append(entry)
+ return agent
+
+
+@pytest.fixture
+def mock_agent() -> MockAgent:
+ """Provide a single-step mock agent for recorder tests."""
+ return make_mock_agent()
+
+
+@pytest.fixture
+def mock_agent_with_screenshot() -> MockAgent:
+ """Provide a mock agent with a base64 screenshot."""
+ return make_mock_agent(screenshot="iVBORw0KGgo=")
+
+
+@pytest.fixture
+def mock_agent_no_action() -> MockAgent:
+ """Provide a mock agent with no action or model output."""
+ return make_mock_agent(action_text=None, thinking=None)
+
+
+@pytest.fixture
+def tmp_output_dir(tmp_path):
+ """Provide a temporary output directory for capsule writing."""
+ return str(tmp_path / "capsule_output")
diff --git a/python/tests/test_capsule.py b/python/tests/test_capsule.py
new file mode 100644
index 0000000..d5fb449
--- /dev/null
+++ b/python/tests/test_capsule.py
@@ -0,0 +1,191 @@
+"""Tests for dbar.capsule.Capsule.
+
+Tests verify loading, diffing, and summarizing capsule data without
+touching the filesystem beyond pytest tmp_path.
+"""
+
+from __future__ import annotations
+
+import json
+import os
+
+import pytest
+
+from dbar.capsule import Capsule
+from dbar.types import CapsuleManifest
+
+
+def _write_capsule_json(path: str, manifest: CapsuleManifest) -> None:
+ """Helper: write a CapsuleManifest to a JSON file."""
+ os.makedirs(os.path.dirname(path), exist_ok=True)
+ with open(path, "w") as f:
+ json.dump(manifest.to_dict(), f)
+
+
+class TestCapsuleLoad:
+ """Verify loading capsules from JSON files."""
+
+ def test_should_load_capsule_when_valid_json(self, tmp_path):
+ """Given a valid capsule.json, when load is called, then a Capsule is returned."""
+ path = str(tmp_path / "capsule.json")
+ manifest = CapsuleManifest(step_count=3, steps=[
+ {"index": 0, "dom_hash": "aaa"},
+ {"index": 1, "dom_hash": "bbb"},
+ {"index": 2, "dom_hash": "ccc"},
+ ])
+ _write_capsule_json(path, manifest)
+ capsule = Capsule.load(path)
+ assert capsule.step_count == 3
+ assert capsule.path == path
+
+ def test_should_raise_when_file_not_found(self):
+ """Given a nonexistent path, when load is called, then FileNotFoundError is raised."""
+ with pytest.raises(FileNotFoundError):
+ Capsule.load("/nonexistent/capsule.json")
+
+ def test_should_raise_when_invalid_json(self, tmp_path):
+ """Given a file with invalid JSON, when load is called, then ValueError is raised."""
+ path = str(tmp_path / "bad.json")
+ with open(path, "w") as f:
+ f.write("not json{{{")
+ with pytest.raises(ValueError, match="Failed to parse"):
+ Capsule.load(path)
+
+ def test_should_compute_size_kb(self, tmp_path):
+ """Given a capsule file, when loaded, then size_kb reflects file size."""
+ path = str(tmp_path / "capsule.json")
+ manifest = CapsuleManifest(step_count=0, steps=[])
+ _write_capsule_json(path, manifest)
+ capsule = Capsule.load(path)
+ file_size = os.path.getsize(path)
+ assert capsule.size_kb == pytest.approx(file_size / 1024, abs=0.01)
+
+
+class TestCapsuleDiff:
+ """Verify step-by-step DOM hash comparison between capsules."""
+
+ def test_should_return_empty_diff_when_capsules_identical(self, tmp_path):
+ """Given two identical capsules, when diff is called, then no divergences are returned."""
+ steps = [{"index": 0, "dom_hash": "aaa"}, {"index": 1, "dom_hash": "bbb"}]
+ path_a = str(tmp_path / "a.json")
+ path_b = str(tmp_path / "b.json")
+ _write_capsule_json(path_a, CapsuleManifest(step_count=2, steps=steps))
+ _write_capsule_json(path_b, CapsuleManifest(step_count=2, steps=steps))
+ a = Capsule.load(path_a)
+ b = Capsule.load(path_b)
+ divergences = a.diff(b)
+ assert divergences == []
+
+ def test_should_detect_divergence_when_dom_hashes_differ(self, tmp_path):
+ """Given capsules with different DOM hashes at step 1, when diff is called, then step 1 is flagged."""
+ path_a = str(tmp_path / "a.json")
+ path_b = str(tmp_path / "b.json")
+ _write_capsule_json(path_a, CapsuleManifest(step_count=2, steps=[
+ {"index": 0, "dom_hash": "aaa"},
+ {"index": 1, "dom_hash": "bbb"},
+ ]))
+ _write_capsule_json(path_b, CapsuleManifest(step_count=2, steps=[
+ {"index": 0, "dom_hash": "aaa"},
+ {"index": 1, "dom_hash": "xxx"},
+ ]))
+ a = Capsule.load(path_a)
+ b = Capsule.load(path_b)
+ divergences = a.diff(b)
+ assert len(divergences) == 1
+ assert divergences[0]["step"] == 1
+ assert divergences[0]["field"] == "dom_hash"
+
+ def test_should_handle_different_step_counts(self, tmp_path):
+ """Given capsules with different step counts, when diff is called, then extra steps are flagged."""
+ path_a = str(tmp_path / "a.json")
+ path_b = str(tmp_path / "b.json")
+ _write_capsule_json(path_a, CapsuleManifest(step_count=3, steps=[
+ {"index": 0, "dom_hash": "aaa"},
+ {"index": 1, "dom_hash": "bbb"},
+ {"index": 2, "dom_hash": "ccc"},
+ ]))
+ _write_capsule_json(path_b, CapsuleManifest(step_count=1, steps=[
+ {"index": 0, "dom_hash": "aaa"},
+ ]))
+ a = Capsule.load(path_a)
+ b = Capsule.load(path_b)
+ divergences = a.diff(b)
+ assert any(d["type"] == "step_count_mismatch" for d in divergences)
+
+ def test_should_detect_screenshot_hash_divergence(self, tmp_path):
+ """Given capsules with different screenshot hashes, when diff is called, then flagged."""
+ path_a = str(tmp_path / "a.json")
+ path_b = str(tmp_path / "b.json")
+ _write_capsule_json(path_a, CapsuleManifest(step_count=1, steps=[
+ {"index": 0, "dom_hash": "aaa", "screenshot_hash": "s1"},
+ ]))
+ _write_capsule_json(path_b, CapsuleManifest(step_count=1, steps=[
+ {"index": 0, "dom_hash": "aaa", "screenshot_hash": "s2"},
+ ]))
+ a = Capsule.load(path_a)
+ b = Capsule.load(path_b)
+ divergences = a.diff(b)
+ assert len(divergences) == 1
+ assert divergences[0]["field"] == "screenshot_hash"
+
+ def test_should_skip_comparison_when_hash_missing_on_both_sides(self, tmp_path):
+ """Given steps with no dom_hash on either side, when diff is called, then no divergence."""
+ path_a = str(tmp_path / "a.json")
+ path_b = str(tmp_path / "b.json")
+ _write_capsule_json(path_a, CapsuleManifest(step_count=1, steps=[
+ {"index": 0},
+ ]))
+ _write_capsule_json(path_b, CapsuleManifest(step_count=1, steps=[
+ {"index": 0},
+ ]))
+ a = Capsule.load(path_a)
+ b = Capsule.load(path_b)
+ divergences = a.diff(b)
+ assert divergences == []
+
+
+class TestCapsuleSummary:
+ """Verify human-readable summary output."""
+
+ def test_should_include_step_count_in_summary(self, tmp_path):
+ """Given a capsule, when summary is called, then step count is included."""
+ path = str(tmp_path / "capsule.json")
+ _write_capsule_json(path, CapsuleManifest(step_count=5, steps=[
+ {"index": i, "dom_hash": f"h{i}"} for i in range(5)
+ ]))
+ capsule = Capsule.load(path)
+ text = capsule.summary()
+ assert "5" in text
+ assert "step" in text.lower()
+
+ def test_should_include_path_in_summary(self, tmp_path):
+ """Given a capsule, when summary is called, then the file path is included."""
+ path = str(tmp_path / "capsule.json")
+ _write_capsule_json(path, CapsuleManifest(step_count=0, steps=[]))
+ capsule = Capsule.load(path)
+ text = capsule.summary()
+ assert str(tmp_path) in text
+
+ def test_should_include_size_in_summary(self, tmp_path):
+ """Given a capsule, when summary is called, then size is mentioned."""
+ path = str(tmp_path / "capsule.json")
+ _write_capsule_json(path, CapsuleManifest(step_count=0, steps=[]))
+ capsule = Capsule.load(path)
+ text = capsule.summary()
+ assert "kb" in text.lower() or "KB" in text
+
+ def test_should_include_version_in_summary(self, tmp_path):
+ """Given a capsule, when summary is called, then version is included."""
+ path = str(tmp_path / "capsule.json")
+ _write_capsule_json(path, CapsuleManifest(version="0.1.0", step_count=0, steps=[]))
+ capsule = Capsule.load(path)
+ text = capsule.summary()
+ assert "0.1.0" in text
+
+ def test_should_report_zero_steps_gracefully(self, tmp_path):
+ """Given a capsule with no steps, when summary is called, then it handles zero gracefully."""
+ path = str(tmp_path / "capsule.json")
+ _write_capsule_json(path, CapsuleManifest(step_count=0, steps=[]))
+ capsule = Capsule.load(path)
+ text = capsule.summary()
+ assert "0" in text
diff --git a/python/tests/test_recorder.py b/python/tests/test_recorder.py
new file mode 100644
index 0000000..a2790c1
--- /dev/null
+++ b/python/tests/test_recorder.py
@@ -0,0 +1,194 @@
+"""Tests for dbar.recorder.DBARRecorder.
+
+Tests verify behavior (output snapshots and capsule files) rather than
+internal implementation details.
+"""
+
+from __future__ import annotations
+
+import hashlib
+import json
+import os
+
+import pytest
+
+from dbar.recorder import DBARRecorder
+from dbar.types import StepSnapshot
+from tests.conftest import make_mock_agent
+
+
+class TestStepCapture:
+ """Verify on_step_end extracts and hashes agent data correctly."""
+
+ @pytest.mark.asyncio
+ async def test_should_capture_step_when_agent_has_history(self, mock_agent, tmp_output_dir):
+ """Given a mock agent with one step, when on_step_end is called, then a snapshot is recorded."""
+ recorder = DBARRecorder(output_dir=tmp_output_dir)
+ await recorder.on_step_end(mock_agent)
+ assert len(recorder._snapshots) == 1
+ assert recorder._snapshots[0].index == 0
+
+ @pytest.mark.asyncio
+ async def test_should_increment_index_when_multiple_steps_captured(self, tmp_output_dir):
+ """Given an agent with multiple steps, when on_step_end is called for each, then indices increment."""
+ agent = make_mock_agent(num_steps=3)
+ recorder = DBARRecorder(output_dir=tmp_output_dir)
+ for _ in range(3):
+ await recorder.on_step_end(agent)
+ assert [s.index for s in recorder._snapshots] == [0, 1, 2]
+
+ @pytest.mark.asyncio
+ async def test_should_hash_dom_with_sha256_when_dom_present(self, mock_agent, tmp_output_dir):
+ """Given DOM content, when captured, then dom_hash is the SHA-256 hex digest."""
+ recorder = DBARRecorder(output_dir=tmp_output_dir)
+ await recorder.on_step_end(mock_agent)
+ dom_text = mock_agent.history.history[-1].state.element_tree.to_string()
+ expected_hash = hashlib.sha256(dom_text.encode("utf-8")).hexdigest()
+ assert recorder._snapshots[0].dom_hash == expected_hash
+
+ @pytest.mark.asyncio
+ async def test_should_hash_screenshot_when_screenshot_present(
+ self, mock_agent_with_screenshot, tmp_output_dir
+ ):
+ """Given a screenshot, when captured, then screenshot_hash is the SHA-256 hex digest."""
+ recorder = DBARRecorder(output_dir=tmp_output_dir, include_screenshots=True)
+ await recorder.on_step_end(mock_agent_with_screenshot)
+ screenshot_data = mock_agent_with_screenshot.history.history[-1].state.screenshot
+ expected_hash = hashlib.sha256(screenshot_data.encode("utf-8")).hexdigest()
+ assert recorder._snapshots[0].screenshot_hash == expected_hash
+
+ @pytest.mark.asyncio
+ async def test_should_set_screenshot_hash_none_when_screenshots_disabled(
+ self, mock_agent_with_screenshot, tmp_output_dir
+ ):
+ """Given screenshots disabled, when captured, then screenshot_hash is None."""
+ recorder = DBARRecorder(output_dir=tmp_output_dir, include_screenshots=False)
+ await recorder.on_step_end(mock_agent_with_screenshot)
+ assert recorder._snapshots[0].screenshot_hash is None
+
+ @pytest.mark.asyncio
+ async def test_should_capture_action_when_action_present(self, mock_agent, tmp_output_dir):
+ """Given an action in the step, when captured, then action is recorded."""
+ recorder = DBARRecorder(output_dir=tmp_output_dir, include_actions=True)
+ await recorder.on_step_end(mock_agent)
+ assert recorder._snapshots[0].action is not None
+
+ @pytest.mark.asyncio
+ async def test_should_skip_action_when_actions_disabled(self, mock_agent, tmp_output_dir):
+ """Given actions disabled, when captured, then action is None."""
+ recorder = DBARRecorder(output_dir=tmp_output_dir, include_actions=False)
+ await recorder.on_step_end(mock_agent)
+ assert recorder._snapshots[0].action is None
+
+ @pytest.mark.asyncio
+ async def test_should_capture_url_when_state_present(self, mock_agent, tmp_output_dir):
+ """Given a page URL, when captured, then url is recorded in snapshot."""
+ recorder = DBARRecorder(output_dir=tmp_output_dir)
+ await recorder.on_step_end(mock_agent)
+ assert recorder._snapshots[0].url == "https://example.com"
+
+ @pytest.mark.asyncio
+ async def test_should_set_dom_hash_none_when_dom_disabled(self, mock_agent, tmp_output_dir):
+ """Given DOM capture disabled, when captured, then dom_hash is None."""
+ recorder = DBARRecorder(output_dir=tmp_output_dir, include_dom=False)
+ await recorder.on_step_end(mock_agent)
+ assert recorder._snapshots[0].dom_hash is None
+
+ @pytest.mark.asyncio
+ async def test_should_capture_thinking_when_enabled(self, tmp_output_dir):
+ """Given thinking enabled and model output has state, when captured, then thinking is recorded."""
+ agent = make_mock_agent(thinking="I need to click the button")
+ recorder = DBARRecorder(output_dir=tmp_output_dir, include_thinking=True)
+ await recorder.on_step_end(agent)
+ assert recorder._snapshots[0].thinking == "I need to click the button"
+
+ @pytest.mark.asyncio
+ async def test_should_skip_thinking_when_disabled(self, tmp_output_dir):
+ """Given thinking disabled, when captured, then thinking is None."""
+ agent = make_mock_agent(thinking="secret thoughts")
+ recorder = DBARRecorder(output_dir=tmp_output_dir, include_thinking=False)
+ await recorder.on_step_end(agent)
+ assert recorder._snapshots[0].thinking is None
+
+
+class TestRedaction:
+ """Verify sensitive data redaction."""
+
+ @pytest.mark.asyncio
+ async def test_should_redact_url_params_when_redact_enabled(self, tmp_output_dir):
+ """Given redact_sensitive=True, when URL has query params, then params are redacted."""
+ agent = make_mock_agent(url="https://example.com/page?token=secret123&id=42")
+ recorder = DBARRecorder(output_dir=tmp_output_dir, redact_sensitive=True)
+ await recorder.on_step_end(agent)
+ url = recorder._snapshots[0].url
+ assert "secret123" not in url
+ assert "REDACTED" in url
+
+
+class TestMissingData:
+ """Verify graceful handling of missing or None data."""
+
+ @pytest.mark.asyncio
+ async def test_should_handle_no_model_output(self, mock_agent_no_action, tmp_output_dir):
+ """Given no model output, when captured, then action and thinking are None."""
+ recorder = DBARRecorder(output_dir=tmp_output_dir)
+ await recorder.on_step_end(mock_agent_no_action)
+ assert recorder._snapshots[0].action is None
+
+ @pytest.mark.asyncio
+ async def test_should_handle_no_screenshot_in_state(self, mock_agent, tmp_output_dir):
+ """Given no screenshot in state, when captured, then screenshot_hash is None."""
+ recorder = DBARRecorder(output_dir=tmp_output_dir, include_screenshots=True)
+ await recorder.on_step_end(mock_agent)
+ assert recorder._snapshots[0].screenshot_hash is None
+
+
+class TestFinish:
+ """Verify capsule writing and finish behavior."""
+
+ @pytest.mark.asyncio
+ async def test_should_write_capsule_json_when_finish_called(self, mock_agent, tmp_output_dir):
+ """Given recorded steps, when finish is called, then capsule.json is written."""
+ recorder = DBARRecorder(output_dir=tmp_output_dir)
+ await recorder.on_step_end(mock_agent)
+ capsule = recorder.finish()
+ capsule_path = os.path.join(tmp_output_dir, "capsule.json")
+ assert os.path.exists(capsule_path)
+ with open(capsule_path) as f:
+ data = json.load(f)
+ assert data["step_count"] == 1
+
+ @pytest.mark.asyncio
+ async def test_should_return_capsule_object_when_finish_called(
+ self, mock_agent, tmp_output_dir
+ ):
+ """Given recorded steps, when finish is called, then a Capsule object is returned."""
+ recorder = DBARRecorder(output_dir=tmp_output_dir)
+ await recorder.on_step_end(mock_agent)
+ capsule = recorder.finish()
+ assert capsule.step_count == 1
+ assert capsule.path == os.path.join(tmp_output_dir, "capsule.json")
+
+ @pytest.mark.asyncio
+ async def test_should_raise_when_finish_called_twice(self, mock_agent, tmp_output_dir):
+ """Given finish already called, when finish is called again, then RuntimeError is raised."""
+ recorder = DBARRecorder(output_dir=tmp_output_dir)
+ await recorder.on_step_end(mock_agent)
+ recorder.finish()
+ with pytest.raises(RuntimeError, match="already finished"):
+ recorder.finish()
+
+ @pytest.mark.asyncio
+ async def test_should_write_capsule_with_zero_steps_when_no_data(self, tmp_output_dir):
+ """Given no steps recorded, when finish is called, then capsule has zero steps."""
+ recorder = DBARRecorder(output_dir=tmp_output_dir)
+ capsule = recorder.finish()
+ assert capsule.step_count == 0
+
+ @pytest.mark.asyncio
+ async def test_should_record_capsule_size_in_kb(self, mock_agent, tmp_output_dir):
+ """Given a written capsule, when finish returns, then size_kb is positive."""
+ recorder = DBARRecorder(output_dir=tmp_output_dir)
+ await recorder.on_step_end(mock_agent)
+ capsule = recorder.finish()
+ assert capsule.size_kb > 0
diff --git a/scripts/publish-npm-if-needed.mjs b/scripts/publish-npm-if-needed.mjs
new file mode 100644
index 0000000..28d9656
--- /dev/null
+++ b/scripts/publish-npm-if-needed.mjs
@@ -0,0 +1,35 @@
+import { execFileSync } from "node:child_process";
+import { readFileSync } from "node:fs";
+import { resolve } from "node:path";
+
+const packageJson = JSON.parse(readFileSync(resolve("package.json"), "utf8"));
+const packageName = packageJson.name;
+const version = packageJson.version;
+
+function isAlreadyPublished(name, currentVersion) {
+ try {
+ const result = execFileSync(
+ "npm",
+ ["view", `${name}@${currentVersion}`, "version"],
+ { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
+ ).trim();
+ return result === currentVersion;
+ } catch {
+ return false;
+ }
+}
+
+if (isAlreadyPublished(packageName, version)) {
+ console.log(`${packageName}@${version} is already published. Skipping npm publish.`);
+ process.exit(0);
+}
+
+execFileSync("npm", ["run", "build"], {
+ stdio: "inherit",
+ env: process.env,
+});
+
+execFileSync("npx", ["changeset", "publish"], {
+ stdio: "inherit",
+ env: process.env,
+});
diff --git a/scripts/sync-python-version.mjs b/scripts/sync-python-version.mjs
new file mode 100644
index 0000000..3a50c92
--- /dev/null
+++ b/scripts/sync-python-version.mjs
@@ -0,0 +1,15 @@
+import { readFileSync, writeFileSync } from "node:fs";
+import { resolve } from "node:path";
+
+const packageJson = JSON.parse(readFileSync(resolve("package.json"), "utf8"));
+const version = packageJson.version;
+const pyprojectPath = resolve("python/pyproject.toml");
+const pyproject = readFileSync(pyprojectPath, "utf8");
+const nextPyproject = pyproject.replace(/^version = "[^"]+"$/m, `version = "${version}"`);
+
+if (nextPyproject !== pyproject) {
+ writeFileSync(pyprojectPath, nextPyproject, "utf8");
+ console.log(`Synced python/pyproject.toml to ${version}`);
+} else {
+ console.log(`python/pyproject.toml already at ${version}`);
+}
diff --git a/src/__tests__/capsule-types.test.ts b/src/__tests__/capsule-types.test.ts
index 4765b6d..a05afe8 100644
--- a/src/__tests__/capsule-types.test.ts
+++ b/src/__tests__/capsule-types.test.ts
@@ -421,7 +421,7 @@ describe("DivergenceTypeSchema", () => {
// Given an unknown divergence type
// When parsed
// Then it throws
- expect(() => DivergenceTypeSchema.parse("screenshot_mismatch")).toThrow();
+ expect(() => DivergenceTypeSchema.parse("nonexistent_type")).toThrow();
});
});
diff --git a/src/__tests__/cli-args.test.ts b/src/__tests__/cli-args.test.ts
new file mode 100644
index 0000000..0429261
--- /dev/null
+++ b/src/__tests__/cli-args.test.ts
@@ -0,0 +1,131 @@
+import { describe, it, expect } from "vitest";
+import { parseArgs } from "../cli/args.js";
+
+describe("parseArgs", () => {
+ it("should parse replay command with capsule path", () => {
+ const result = parseArgs(["node", "dbar", "replay", "my.capsule"]);
+ expect(result).toEqual({
+ command: "replay",
+ capsulePath: "my.capsule",
+ options: { cost: false, json: false },
+ });
+ });
+
+ it("should parse replay command with --cost flag", () => {
+ const result = parseArgs(["node", "dbar", "replay", "my.capsule", "--cost"]);
+ expect(result).toEqual({
+ command: "replay",
+ capsulePath: "my.capsule",
+ options: { cost: true, json: false },
+ });
+ });
+
+ it("should parse replay command with --json flag", () => {
+ const result = parseArgs(["node", "dbar", "replay", "my.capsule", "--json"]);
+ expect(result).toEqual({
+ command: "replay",
+ capsulePath: "my.capsule",
+ options: { cost: false, json: true },
+ });
+ });
+
+ it("should parse replay command with both --cost and --json", () => {
+ const result = parseArgs(["node", "dbar", "replay", "my.capsule", "--cost", "--json"]);
+ expect(result).toEqual({
+ command: "replay",
+ capsulePath: "my.capsule",
+ options: { cost: true, json: true },
+ });
+ });
+
+ it("should parse eval command with required options", () => {
+ const result = parseArgs([
+ "node", "dbar", "eval",
+ "--capsules", "./caps",
+ "--assertions", "./asserts.yaml",
+ ]);
+ expect(result).toEqual({
+ command: "eval",
+ options: { capsules: "./caps", assertions: "./asserts.yaml", json: false },
+ });
+ });
+
+ it("should parse eval command with --json flag", () => {
+ const result = parseArgs([
+ "node", "dbar", "eval",
+ "--capsules", "./caps",
+ "--assertions", "./asserts.yaml",
+ "--json",
+ ]);
+ expect(result).toEqual({
+ command: "eval",
+ options: { capsules: "./caps", assertions: "./asserts.yaml", json: true },
+ });
+ });
+
+ it("should parse validate command with capsule path", () => {
+ const result = parseArgs(["node", "dbar", "validate", "my.capsule"]);
+ expect(result).toEqual({
+ command: "validate",
+ capsulePath: "my.capsule",
+ });
+ });
+
+ it("should parse --help flag", () => {
+ const result = parseArgs(["node", "dbar", "--help"]);
+ expect(result).toEqual({ command: "help" });
+ });
+
+ it("should parse --version flag", () => {
+ const result = parseArgs(["node", "dbar", "--version"]);
+ expect(result).toEqual({ command: "version" });
+ });
+
+ it("should return error when no command provided", () => {
+ const result = parseArgs(["node", "dbar"]);
+ expect(result).toEqual({
+ command: "error",
+ message: 'No command provided. Run "dbar --help" for usage.',
+ });
+ });
+
+ it("should return error when replay missing capsule path", () => {
+ const result = parseArgs(["node", "dbar", "replay"]);
+ expect(result).toEqual({
+ command: "error",
+ message: 'replay requires a capsule path. Usage: dbar replay [--cost] [--json]',
+ });
+ });
+
+ it("should return error when eval missing --capsules", () => {
+ const result = parseArgs(["node", "dbar", "eval", "--assertions", "a.yaml"]);
+ expect(result).toEqual({
+ command: "error",
+ message: 'eval requires --capsules and --assertions. Usage: dbar eval --capsules --assertions [--json]',
+ });
+ });
+
+ it("should return error when eval missing --assertions", () => {
+ const result = parseArgs(["node", "dbar", "eval", "--capsules", "./caps"]);
+ expect(result).toEqual({
+ command: "error",
+ message: 'eval requires --capsules and --assertions. Usage: dbar eval --capsules --assertions [--json]',
+ });
+ });
+
+ it("should return error when validate missing capsule path", () => {
+ const result = parseArgs(["node", "dbar", "validate"]);
+ expect(result).toEqual({
+ command: "error",
+ message: 'validate requires a capsule path. Usage: dbar validate ',
+ });
+ });
+
+ it("should return error for unknown command", () => {
+ const result = parseArgs(["node", "dbar", "unknown"]);
+ expect(result).toEqual({
+ command: "error",
+ message: 'Unknown command "unknown". Run "dbar --help" for usage.',
+ });
+ });
+});
diff --git a/src/__tests__/cli-cost.test.ts b/src/__tests__/cli-cost.test.ts
new file mode 100644
index 0000000..687e995
--- /dev/null
+++ b/src/__tests__/cli-cost.test.ts
@@ -0,0 +1,78 @@
+import { describe, it, expect } from "vitest";
+import { calculateCost } from "../cli/cost.js";
+
+describe("calculateCost", () => {
+ it("should calculate cost with output token ratio for honest cost model", () => {
+ const result = calculateCost({
+ networkRequestCount: 47,
+ domSnapshotSizes: [4000, 8000, 12000],
+ stepCount: 3,
+ });
+
+ // Tokens: sum of sizes / 4 = 24000 / 4 = 6000
+ // LLM input cost: 6000 * 3 / 1_000_000 = 0.018
+ // LLM output cost: (6000 * 0.15) * 15 / 1_000_000 = 900 * 15 / 1_000_000 = 0.0135
+ // LLM total: 0.0315
+ // Compute: 3 * 2 * 0.0000463 = 0.0002778
+ // Replay compute: 3 * 2 * 0.0000463 = 0.0002778
+ expect(result.estimatedTokens).toBe(6000);
+ expect(result.llmCost).toBeCloseTo(0.0315, 4);
+ expect(result.computeCost).toBeCloseTo(0.0002778, 6);
+ expect(result.replayComputeCost).toBeCloseTo(0.0002778, 6);
+ expect(result.replayCost).toBeCloseTo(result.replayComputeCost, 6);
+ expect(result.totalOriginalCost).toBeCloseTo(result.llmCost + result.computeCost, 6);
+ expect(result.apiSavings).toBeCloseTo(result.totalOriginalCost - result.replayComputeCost, 6);
+ expect(result.apiSavingsPercent).toBeCloseTo(
+ ((result.totalOriginalCost - result.replayComputeCost) / result.totalOriginalCost) * 100,
+ 1
+ );
+ });
+
+ it("should return zero costs when capsule has no steps and no requests", () => {
+ const result = calculateCost({
+ networkRequestCount: 0,
+ domSnapshotSizes: [],
+ stepCount: 0,
+ });
+
+ expect(result.estimatedTokens).toBe(0);
+ expect(result.llmCost).toBe(0);
+ expect(result.computeCost).toBe(0);
+ expect(result.totalOriginalCost).toBe(0);
+ expect(result.replayComputeCost).toBe(0);
+ expect(result.replayCost).toBe(0);
+ expect(result.apiSavings).toBe(0);
+ expect(result.apiSavingsPercent).toBe(0);
+ });
+
+ it("should handle single step with small DOM snapshot", () => {
+ const result = calculateCost({
+ networkRequestCount: 1,
+ domSnapshotSizes: [400],
+ stepCount: 1,
+ });
+
+ expect(result.estimatedTokens).toBe(100); // 400 / 4
+ expect(result.stepCount).toBe(1);
+ expect(result.networkRequestCount).toBe(1);
+ });
+
+ it("should include all fields in the breakdown", () => {
+ const result = calculateCost({
+ networkRequestCount: 10,
+ domSnapshotSizes: [2000],
+ stepCount: 2,
+ });
+
+ expect(result).toHaveProperty("estimatedTokens");
+ expect(result).toHaveProperty("llmCost");
+ expect(result).toHaveProperty("computeCost");
+ expect(result).toHaveProperty("totalOriginalCost");
+ expect(result).toHaveProperty("replayComputeCost");
+ expect(result).toHaveProperty("replayCost");
+ expect(result).toHaveProperty("apiSavings");
+ expect(result).toHaveProperty("apiSavingsPercent");
+ expect(result).toHaveProperty("stepCount");
+ expect(result).toHaveProperty("networkRequestCount");
+ });
+});
diff --git a/src/__tests__/cli-eval.test.ts b/src/__tests__/cli-eval.test.ts
new file mode 100644
index 0000000..f00d241
--- /dev/null
+++ b/src/__tests__/cli-eval.test.ts
@@ -0,0 +1,288 @@
+import { describe, it, expect } from "vitest";
+import { checkAssertion, type Assertion } from "../cli/eval.js";
+import type { CapsuleArchive } from "../capsule/builder.js";
+import type { DeterminismCapsule } from "../capsule/types.js";
+
+// Minimal capsule fixture for assertion checking
+function makeCapsule(overrides?: Partial): CapsuleArchive {
+ const manifest: DeterminismCapsule = {
+ version: "1.0.0",
+ capsuleProfile: "replay",
+ id: "00000000-0000-0000-0000-000000000001",
+ createdAt: "2024-01-01T00:00:00.000Z",
+ environment: {
+ browserBuild: "chromium/1140",
+ browserFlags: [],
+ locale: "en-US",
+ timezone: "UTC",
+ viewport: { width: 1280, height: 720 },
+ deviceScaleFactor: 1,
+ userAgent: "Mozilla/5.0",
+ offline: false,
+ },
+ seeds: { initialTime: 1700000000000 },
+ initialState: {
+ url: "https://example.com",
+ cookies: [],
+ localStorage: [],
+ unsupportedState: ["sessionStorage"],
+ },
+ networkTranscript: {
+ orderingPolicy: "creation",
+ entries: [
+ {
+ index: 0, requestId: "r1", url: "https://example.com/api",
+ method: "GET", headers: {}, requestHash: "h1", occurrenceIndex: 0, timestamp: 0,
+ response: { status: 200, headers: {}, body: "network/abc", bodyHash: "abc" },
+ },
+ {
+ index: 1, requestId: "r2", url: "https://example.com/style.css",
+ method: "GET", headers: {}, requestHash: "h2", occurrenceIndex: 0, timestamp: 1,
+ response: { status: 200, headers: {}, body: "network/def", bodyHash: "def" },
+ },
+ ],
+ },
+ steps: [
+ {
+ index: 0, label: "loaded",
+ observables: {
+ domSnapshotHash: "dom0", accessibilityHash: "a11y0",
+ screenshotHash: "ss0", networkDigest: "nd0",
+ },
+ artifacts: {
+ domSnapshot: "snapshots/0/dom.json",
+ accessibilityYaml: "snapshots/0/accessibility.json",
+ screenshot: "snapshots/0/screenshot.png",
+ },
+ },
+ {
+ index: 1, label: "after-login",
+ observables: {
+ domSnapshotHash: "dom1", accessibilityHash: "a11y1",
+ screenshotHash: "ss1", networkDigest: "nd1",
+ },
+ artifacts: {
+ domSnapshot: "snapshots/1/dom.json",
+ accessibilityYaml: "snapshots/1/accessibility.json",
+ screenshot: "snapshots/1/screenshot.png",
+ },
+ },
+ ],
+ metrics: {
+ totalSteps: 2, totalNetworkRequests: 2,
+ unsupportedRequestCount: 0, captureOverheadMs: 100, capsuleSizeBytes: 500,
+ },
+ ...overrides,
+ };
+
+ const files = new Map();
+ files.set("capsule.json", Buffer.from(JSON.stringify(manifest)));
+ files.set("snapshots/0/dom.json", Buffer.from("{}"));
+ files.set("snapshots/0/accessibility.json", Buffer.from(JSON.stringify({ role: "WebArea", name: "Welcome Dashboard" })));
+ files.set("snapshots/0/screenshot.png", Buffer.from("PNG"));
+ files.set("snapshots/1/dom.json", Buffer.from("{}"));
+ files.set("snapshots/1/accessibility.json", Buffer.from(JSON.stringify({ role: "WebArea", name: "User Profile" })));
+ files.set("snapshots/1/screenshot.png", Buffer.from("PNG"));
+
+ return { manifest, files };
+}
+
+describe("checkAssertion", () => {
+ it("should pass url_contains when URL matches", () => {
+ const archive = makeCapsule({
+ initialState: {
+ url: "https://example.com/dashboard",
+ cookies: [], localStorage: [], unsupportedState: ["sessionStorage"],
+ },
+ });
+ const assertion: Assertion = {
+ step: "loaded",
+ expect: { url_contains: "example.com" },
+ };
+
+ const result = checkAssertion(archive, assertion);
+ expect(result.passed).toBe(true);
+ });
+
+ it("should fail url_contains when URL does not match", () => {
+ const archive = makeCapsule();
+ const assertion: Assertion = {
+ step: "loaded",
+ expect: { url_contains: "/results" },
+ };
+
+ const result = checkAssertion(archive, assertion);
+ expect(result.passed).toBe(false);
+ expect(result.message).toContain("/results");
+ });
+
+ it("should pass dom_hash_stable when hash is non-empty", () => {
+ const archive = makeCapsule();
+ const assertion: Assertion = {
+ step: "loaded",
+ expect: { dom_hash_stable: true },
+ };
+
+ const result = checkAssertion(archive, assertion);
+ expect(result.passed).toBe(true);
+ });
+
+ it("should pass accessibility_contains when text is found", () => {
+ const archive = makeCapsule();
+ const assertion: Assertion = {
+ step: "loaded",
+ expect: { accessibility_contains: "Welcome" },
+ };
+
+ const result = checkAssertion(archive, assertion);
+ expect(result.passed).toBe(true);
+ });
+
+ it("should fail accessibility_contains when text is not found", () => {
+ const archive = makeCapsule();
+ const assertion: Assertion = {
+ step: "loaded",
+ expect: { accessibility_contains: "Nonexistent" },
+ };
+
+ const result = checkAssertion(archive, assertion);
+ expect(result.passed).toBe(false);
+ expect(result.message).toContain("Nonexistent");
+ });
+
+ it("should pass network_count_gte when count meets threshold", () => {
+ const archive = makeCapsule();
+ const assertion: Assertion = {
+ step: "loaded",
+ expect: { network_count_gte: 2 },
+ };
+
+ const result = checkAssertion(archive, assertion);
+ expect(result.passed).toBe(true);
+ });
+
+ it("should fail network_count_gte when count is below threshold", () => {
+ const archive = makeCapsule();
+ const assertion: Assertion = {
+ step: "loaded",
+ expect: { network_count_gte: 10 },
+ };
+
+ const result = checkAssertion(archive, assertion);
+ expect(result.passed).toBe(false);
+ });
+
+ it("should pass network_count_lte when count is within limit", () => {
+ const archive = makeCapsule();
+ const assertion: Assertion = {
+ step: "loaded",
+ expect: { network_count_lte: 5 },
+ };
+
+ const result = checkAssertion(archive, assertion);
+ expect(result.passed).toBe(true);
+ });
+
+ it("should fail network_count_lte when count exceeds limit", () => {
+ const archive = makeCapsule();
+ const assertion: Assertion = {
+ step: "loaded",
+ expect: { network_count_lte: 1 },
+ };
+
+ const result = checkAssertion(archive, assertion);
+ expect(result.passed).toBe(false);
+ });
+
+ it("should pass screenshot_exists when screenshot artifact exists", () => {
+ const archive = makeCapsule();
+ const assertion: Assertion = {
+ step: "loaded",
+ expect: { screenshot_exists: true },
+ };
+
+ const result = checkAssertion(archive, assertion);
+ expect(result.passed).toBe(true);
+ });
+
+ it("should fail screenshot_exists when screenshot artifact is missing", () => {
+ const archive = makeCapsule();
+ archive.files.delete("snapshots/0/screenshot.png");
+ const assertion: Assertion = {
+ step: "loaded",
+ expect: { screenshot_exists: true },
+ };
+
+ const result = checkAssertion(archive, assertion);
+ expect(result.passed).toBe(false);
+ });
+
+ it("should return error when step label is not found", () => {
+ const archive = makeCapsule();
+ const assertion: Assertion = {
+ step: "nonexistent-step",
+ expect: { dom_hash_stable: true },
+ };
+
+ const result = checkAssertion(archive, assertion);
+ expect(result.passed).toBe(false);
+ expect(result.message).toContain("nonexistent-step");
+ });
+
+ it("should check multiple expectations in a single assertion", () => {
+ const archive = makeCapsule();
+ const assertion: Assertion = {
+ step: "loaded",
+ expect: {
+ dom_hash_stable: true,
+ screenshot_exists: true,
+ network_count_gte: 1,
+ },
+ };
+
+ const result = checkAssertion(archive, assertion);
+ expect(result.passed).toBe(true);
+ });
+
+ it("should fail if any expectation in a multi-check assertion fails", () => {
+ const archive = makeCapsule();
+ const assertion: Assertion = {
+ step: "loaded",
+ expect: {
+ dom_hash_stable: true,
+ url_contains: "/nonexistent",
+ },
+ };
+
+ const result = checkAssertion(archive, assertion);
+ expect(result.passed).toBe(false);
+ });
+
+ it("should pass initial_url_contains as alias for url_contains", () => {
+ const archive = makeCapsule({
+ initialState: {
+ url: "https://example.com/dashboard",
+ cookies: [], localStorage: [], unsupportedState: ["sessionStorage"],
+ },
+ });
+ const assertion: Assertion = {
+ step: "loaded",
+ expect: { initial_url_contains: "example.com" },
+ };
+
+ const result = checkAssertion(archive, assertion);
+ expect(result.passed).toBe(true);
+ });
+
+ it("should fail initial_url_contains when URL does not match", () => {
+ const archive = makeCapsule();
+ const assertion: Assertion = {
+ step: "loaded",
+ expect: { initial_url_contains: "/nonexistent" },
+ };
+
+ const result = checkAssertion(archive, assertion);
+ expect(result.passed).toBe(false);
+ expect(result.message).toContain("initial_url_contains");
+ });
+});
diff --git a/src/__tests__/coordinator.test.ts b/src/__tests__/coordinator.test.ts
index 653ffe6..f402bb5 100644
--- a/src/__tests__/coordinator.test.ts
+++ b/src/__tests__/coordinator.test.ts
@@ -129,12 +129,26 @@ describe("Coordinator", () => {
it("shouldStartRecorderAndTimeVirtualizer", async () => {
// Given a page
// When starting capture
- await Coordinator.startCapture(page as any);
+ const state = await Coordinator.startCapture(page as any);
- // Then CDP domains are enabled (Fetch.enable from recorder, Emulation.setVirtualTimePolicy from virtualizer)
- const sendCalls = cdp.send.mock.calls.map((c: any[]) => c[0]);
+ // Then recorder CDP domains are enabled immediately
+ let sendCalls = cdp.send.mock.calls.map((c: any[]) => c[0]);
expect(sendCalls).toContain("Fetch.enable");
expect(sendCalls).toContain("Network.enable");
+ // TimeVirtualizer is deferred to first step() call
+ expect(sendCalls).not.toContain("Emulation.setVirtualTimePolicy");
+
+ // When taking the first step, virtualizer starts
+ cdp.send.mockImplementation(async (method: string) => {
+ if (method === "DOM.getDocument") return { root: { nodeId: 1 } };
+ if (method === "DOM.getOuterHTML") return { outerHTML: "" };
+ if (method === "DOMSnapshot.captureSnapshot") {
+ return { documents: [], strings: [] };
+ }
+ return {};
+ });
+ await Coordinator.step(state, "test-step");
+ sendCalls = cdp.send.mock.calls.map((c: any[]) => c[0]);
expect(sendCalls).toContain("Emulation.setVirtualTimePolicy");
});
});
@@ -145,6 +159,8 @@ describe("Coordinator", () => {
beforeEach(async () => {
// Mock CDP responses needed during step
cdp.send.mockImplementation(async (method: string) => {
+ if (method === "DOM.getDocument") return { root: { nodeId: 1 } };
+ if (method === "DOM.getOuterHTML") return { outerHTML: "" };
if (method === "DOMSnapshot.captureSnapshot") {
return { documents: [], strings: [] };
}
diff --git a/src/__tests__/snapshot.test.ts b/src/__tests__/snapshot.test.ts
index a977a7c..992ef88 100644
--- a/src/__tests__/snapshot.test.ts
+++ b/src/__tests__/snapshot.test.ts
@@ -8,9 +8,11 @@ import type { InitialState } from "../capsule/types.js";
// -- DOM snapshot tests --
-function createMockCDPSessionForDOM(snapshotData: unknown) {
+function createMockCDPSessionForDOM(snapshotData: unknown, outerHTML = "test") {
return {
send: vi.fn().mockImplementation((method: string) => {
+ if (method === "DOM.getDocument") return Promise.resolve({ root: { nodeId: 1 } });
+ if (method === "DOM.getOuterHTML") return Promise.resolve({ outerHTML });
if (method === "DOMSnapshot.enable") return Promise.resolve();
if (method === "DOMSnapshot.captureSnapshot") return Promise.resolve(snapshotData);
return Promise.resolve();
@@ -31,33 +33,35 @@ describe("captureDOMSnapshot", () => {
});
it("shouldReturnSnapshotWithDeterministicHash", async () => {
- // Given a CDP session returning a known snapshot
+ // Given a CDP session returning a known snapshot and HTML
const data = { documents: [{ nodes: [1, 2] }], strings: ["a", "b"] };
- const cdp = createMockCDPSessionForDOM(data);
+ const html = "hello";
+ const cdp = createMockCDPSessionForDOM(data, html);
// When capturing
const result = await captureDOMSnapshot(cdp as any);
- // Then the hash is a valid SHA-256 hex string
+ // Then the hash is a valid SHA-256 hex string based on outerHTML
expect(result.hash).toMatch(/^[a-f0-9]{64}$/);
expect(result.snapshot).toEqual(data);
+ expect(result.serialized).toBe(html);
- // And the hash matches manual computation of the serialized form
- const expectedHash = createHash("sha256").update(result.serialized).digest("hex");
+ // And the hash matches manual computation of the HTML
+ const expectedHash = createHash("sha256").update(html).digest("hex");
expect(result.hash).toBe(expectedHash);
});
- it("shouldProduceSameHashForSameData", async () => {
- // Given two CDP sessions returning identical data
- const data = { zebra: 1, alpha: 2 };
- const cdp1 = createMockCDPSessionForDOM(data);
- const cdp2 = createMockCDPSessionForDOM({ zebra: 1, alpha: 2 });
+ it("shouldProduceSameHashForSameHTML", async () => {
+ // Given two CDP sessions returning different snapshot data but same HTML
+ const html = "test";
+ const cdp1 = createMockCDPSessionForDOM({ zebra: 1 }, html);
+ const cdp2 = createMockCDPSessionForDOM({ alpha: 2 }, html);
// When capturing both
const r1 = await captureDOMSnapshot(cdp1 as any);
const r2 = await captureDOMSnapshot(cdp2 as any);
- // Then hashes are identical
+ // Then hashes are identical (based on HTML, not CDP snapshot)
expect(r1.hash).toBe(r2.hash);
});
@@ -72,7 +76,7 @@ describe("captureDOMSnapshot", () => {
expect(cdp.send).toHaveBeenCalledWith("DOMSnapshot.captureSnapshot", {
computedStyles: ["display", "visibility", "opacity", "position"],
includePaintOrder: false,
- includeDOMRects: true,
+ includeDOMRects: false,
});
});
});
@@ -131,11 +135,11 @@ describe("captureAccessibilitySnapshot", () => {
// Given a page whose accessibility tree is null (empty page)
const page = createMockPageForA11y(null);
- // When capturing
+ // When capturing (null from strategy 1 triggers fallback to strategy 3,
+ // which fails on the mock → falls through to the empty tree fallback)
const result = await captureAccessibilitySnapshot(page as any);
- // Then it returns a hash of "null"
- expect(result.tree).toBeNull();
+ // Then it returns a valid hash (from the fallback tree)
expect(result.hash).toMatch(/^[a-f0-9]{64}$/);
});
});
diff --git a/src/capsule/types.ts b/src/capsule/types.ts
index f8ba849..46bbe6f 100644
--- a/src/capsule/types.ts
+++ b/src/capsule/types.ts
@@ -224,6 +224,7 @@ export type DeterminismCapsule = z.infer;
export const DivergenceTypeSchema = z.enum([
"dom_mismatch",
"accessibility_mismatch",
+ "screenshot_mismatch",
"network_digest_mismatch",
"unmatched_request",
"unsupported_traffic",
diff --git a/src/cli.ts b/src/cli.ts
new file mode 100644
index 0000000..ed0fe1c
--- /dev/null
+++ b/src/cli.ts
@@ -0,0 +1,97 @@
+#!/usr/bin/env node
+/**
+ * DBAR CLI — Deterministic Browser Agent Runtime command-line interface.
+ *
+ * Commands:
+ * dbar replay [--cost] [--json] Replay a capsule
+ * dbar eval --capsules --assertions Evaluate capsules against assertions
+ * dbar validate Validate a capsule
+ * dbar --help Show help
+ * dbar --version Show version
+ */
+
+import { readFile } from "node:fs/promises";
+import { parseArgs } from "./cli/args.js";
+import { runReplay } from "./cli/replay.js";
+import { runEval } from "./cli/run-eval.js";
+import { deserializeCapsuleArchive } from "./capsule/builder.js";
+import { validateCapsule } from "./capsule/validator.js";
+
+const HELP_TEXT = `DBAR — Deterministic Browser Agent Runtime
+
+Usage:
+ dbar replay [--cost] [--json]
+ Replay a capsule and output results.
+ --cost Show cost comparison (original vs. replay)
+ --json Output results as JSON
+
+ dbar eval --capsules --assertions [--json]
+ Evaluate capsules against assertions.
+
+ dbar validate
+ Validate a capsule for structural integrity.
+
+ dbar --help Show this help message
+ dbar --version Show version
+`;
+
+async function main(): Promise {
+ const cmd = parseArgs(process.argv);
+
+ switch (cmd.command) {
+ case "help":
+ process.stdout.write(HELP_TEXT);
+ return;
+
+ case "version": {
+ const pkgPath = new URL("../package.json", import.meta.url);
+ const pkg = JSON.parse(await readFile(pkgPath, "utf-8")) as { version: string };
+ process.stdout.write(pkg.version + "\n");
+ return;
+ }
+
+ case "replay":
+ await runReplay(cmd.capsulePath, cmd.options);
+ return;
+
+ case "eval":
+ await runEval(cmd.options);
+ return;
+
+ case "validate": {
+ const raw = await readFile(cmd.capsulePath, "utf-8");
+ const archive = deserializeCapsuleArchive(raw);
+ const result = validateCapsule(archive);
+
+ if (result.valid) {
+ process.stdout.write("Capsule is valid.\n");
+ if (result.warnings.length > 0) {
+ for (const w of result.warnings) {
+ process.stderr.write(`Warning [${w.path}]: ${w.message}\n`);
+ }
+ }
+ } else {
+ process.stderr.write("Capsule validation failed:\n");
+ for (const e of result.errors) {
+ process.stderr.write(` Error [${e.path}]: ${e.message}\n`);
+ }
+ for (const w of result.warnings) {
+ process.stderr.write(` Warning [${w.path}]: ${w.message}\n`);
+ }
+ process.exitCode = 1;
+ }
+ return;
+ }
+
+ case "error":
+ process.stderr.write("Error: " + cmd.message + "\n");
+ process.exitCode = 1;
+ return;
+ }
+}
+
+main().catch((err: unknown) => {
+ const message = err instanceof Error ? err.message : String(err);
+ process.stderr.write("Fatal: " + message + "\n");
+ process.exitCode = 1;
+});
diff --git a/src/cli/args.ts b/src/cli/args.ts
new file mode 100644
index 0000000..5581071
--- /dev/null
+++ b/src/cli/args.ts
@@ -0,0 +1,95 @@
+/**
+ * Raw argv parser for the DBAR CLI. No external dependencies (no commander/yargs).
+ *
+ * @example
+ * ```ts
+ * const cmd = parseArgs(process.argv);
+ * if (cmd.command === "replay") { ... }
+ * ```
+ */
+
+/** Discriminated union of all parsed CLI commands. */
+export type ParsedCommand =
+ | { command: "replay"; capsulePath: string; options: { cost: boolean; json: boolean } }
+ | { command: "eval"; options: { capsules: string; assertions: string; json: boolean } }
+ | { command: "validate"; capsulePath: string }
+ | { command: "help" }
+ | { command: "version" }
+ | { command: "error"; message: string };
+
+/**
+ * Parse process.argv into a structured command descriptor.
+ *
+ * @param argv - Raw argv array (includes node binary and script path at [0] and [1]).
+ * @returns A {@link ParsedCommand} describing which subcommand to run and its arguments.
+ */
+export function parseArgs(argv: string[]): ParsedCommand {
+ const args = argv.slice(2);
+
+ if (args.length === 0) {
+ return { command: "error", message: 'No command provided. Run "dbar --help" for usage.' };
+ }
+
+ const first = args[0]!;
+
+ if (first === "--help" || first === "-h") {
+ return { command: "help" };
+ }
+ if (first === "--version" || first === "-v") {
+ return { command: "version" };
+ }
+
+ if (first === "replay") {
+ const positional = args.slice(1).find((a) => !a.startsWith("--"));
+ if (!positional) {
+ return {
+ command: "error",
+ message: "replay requires a capsule path. Usage: dbar replay [--cost] [--json]",
+ };
+ }
+ return {
+ command: "replay",
+ capsulePath: positional,
+ options: {
+ cost: args.includes("--cost"),
+ json: args.includes("--json"),
+ },
+ };
+ }
+
+ if (first === "eval") {
+ const capsulesIdx = args.indexOf("--capsules");
+ const assertionsIdx = args.indexOf("--assertions");
+ const capsules = capsulesIdx >= 0 ? args[capsulesIdx + 1] : undefined;
+ const assertions = assertionsIdx >= 0 ? args[assertionsIdx + 1] : undefined;
+
+ if (!capsules || !assertions) {
+ return {
+ command: "error",
+ message: "eval requires --capsules and --assertions. Usage: dbar eval --capsules --assertions [--json]",
+ };
+ }
+
+ return {
+ command: "eval",
+ options: {
+ capsules,
+ assertions,
+ json: args.includes("--json"),
+ },
+ };
+ }
+
+ if (first === "validate") {
+ const positional = args.slice(1).find((a) => !a.startsWith("--"));
+ if (!positional) {
+ return {
+ command: "error",
+ message: "validate requires a capsule path. Usage: dbar validate ",
+ };
+ }
+ return { command: "validate", capsulePath: positional };
+ }
+
+ return { command: "error", message: `Unknown command "${first}". Run "dbar --help" for usage.` };
+}
diff --git a/src/cli/cost.ts b/src/cli/cost.ts
new file mode 100644
index 0000000..294a367
--- /dev/null
+++ b/src/cli/cost.ts
@@ -0,0 +1,99 @@
+/**
+ * Cost estimation for DBAR replay savings.
+ *
+ * Compares the estimated cost of an original browser agent run (LLM tokens +
+ * compute) against a deterministic replay (no LLM calls, all network mocked,
+ * but browser compute cost remains).
+ */
+
+/** Input data extracted from a capsule for cost estimation. */
+export interface CostInput {
+ /** Total number of network requests in the capsule transcript. */
+ networkRequestCount: number;
+ /** Byte sizes of each step's DOM snapshot (used to estimate LLM token count). */
+ domSnapshotSizes: number[];
+ /** Number of steps in the capsule. */
+ stepCount: number;
+}
+
+/** Itemized cost breakdown comparing original run vs. replay. */
+export interface CostBreakdown {
+ /** Estimated LLM token count (sum of DOM snapshot chars / 4). */
+ estimatedTokens: number;
+ /** Estimated LLM API cost in USD (input + output at Claude Sonnet rates). */
+ llmCost: number;
+ /** Estimated browser compute cost in USD (stepCount * 2s * vCPU rate). */
+ computeCost: number;
+ /** Total estimated original run cost in USD. */
+ totalOriginalCost: number;
+ /** Replay compute cost in USD (replay still uses browser compute). */
+ replayComputeCost: number;
+ /** Replay cost — equals replayComputeCost (no LLM calls, but compute remains). */
+ replayCost: number;
+ /** API savings in USD (totalOriginalCost - replayComputeCost). */
+ apiSavings: number;
+ /** API savings as a percentage of totalOriginalCost. */
+ apiSavingsPercent: number;
+ /** Number of steps (pass-through for display). */
+ stepCount: number;
+ /** Number of network requests (pass-through for display). */
+ networkRequestCount: number;
+}
+
+// Rates: Claude Sonnet 4 ($3/1M input, $15/1M output) as of March 2026
+const INPUT_COST_PER_TOKEN = 3 / 1_000_000;
+const OUTPUT_COST_PER_TOKEN = 15 / 1_000_000;
+
+// Browser agents output ~15% of input tokens (mostly action commands, not prose)
+const OUTPUT_TOKEN_RATIO = 0.15;
+
+// Cloud browser compute: ~$0.0000463 per vCPU-second
+const VCPU_COST_PER_SECOND = 0.0000463;
+
+// Estimated seconds of compute per step
+const SECONDS_PER_STEP = 2;
+
+/**
+ * Calculate cost comparison between an original browser agent run and a
+ * deterministic replay from a capsule.
+ *
+ * @param input - Capsule metrics needed for cost estimation.
+ * @returns Itemized {@link CostBreakdown} with original vs. replay costs.
+ *
+ * @example
+ * ```ts
+ * const breakdown = calculateCost({
+ * networkRequestCount: 47,
+ * domSnapshotSizes: [4000, 8000],
+ * stepCount: 2,
+ * });
+ * console.log(`Savings: $${breakdown.savings.toFixed(2)}`);
+ * ```
+ */
+export function calculateCost(input: CostInput): CostBreakdown {
+ const totalChars = input.domSnapshotSizes.reduce((sum, size) => sum + size, 0);
+ const estimatedTokens = Math.floor(totalChars / 4);
+
+ const llmCost =
+ estimatedTokens * INPUT_COST_PER_TOKEN +
+ (estimatedTokens * OUTPUT_TOKEN_RATIO) * OUTPUT_COST_PER_TOKEN;
+ const computeCost = input.stepCount * SECONDS_PER_STEP * VCPU_COST_PER_SECOND;
+ const totalOriginalCost = llmCost + computeCost;
+ const replayComputeCost = input.stepCount * SECONDS_PER_STEP * VCPU_COST_PER_SECOND;
+ const apiSavings = totalOriginalCost - replayComputeCost;
+ const apiSavingsPercent =
+ totalOriginalCost > 0 ? (apiSavings / totalOriginalCost) * 100 : 0;
+
+ return {
+ estimatedTokens,
+ llmCost,
+ computeCost,
+ totalOriginalCost,
+ replayComputeCost,
+ replayCost: replayComputeCost,
+ apiSavings,
+ apiSavingsPercent,
+ stepCount: input.stepCount,
+ networkRequestCount: input.networkRequestCount,
+ };
+}
diff --git a/src/cli/eval.ts b/src/cli/eval.ts
new file mode 100644
index 0000000..0074846
--- /dev/null
+++ b/src/cli/eval.ts
@@ -0,0 +1,143 @@
+/**
+ * Eval command: check capsule step snapshots against YAML assertions.
+ *
+ * Assertions are checked against capsule metadata without live replay —
+ * they verify structural properties of the captured session.
+ */
+
+import type { CapsuleArchive } from "../capsule/builder.js";
+
+/** A single expectation in an assertion. */
+export interface AssertionExpect {
+ url_contains?: string;
+ /** Alias for url_contains — clearer name since it checks initialState.url only. */
+ initial_url_contains?: string;
+ dom_hash_stable?: boolean;
+ accessibility_contains?: string;
+ network_count_gte?: number;
+ network_count_lte?: number;
+ screenshot_exists?: boolean;
+}
+
+/** An assertion targeting a step by label. */
+export interface Assertion {
+ step: string;
+ expect: AssertionExpect;
+}
+
+/** Result of checking one assertion against one capsule. */
+export interface AssertionResult {
+ passed: boolean;
+ step: string;
+ /** Human-readable failure reason, present only when `passed` is false. */
+ message?: string;
+}
+
+/**
+ * Check a single assertion against a capsule archive.
+ *
+ * Finds the step by label, then evaluates each expectation key against
+ * the capsule's metadata and artifact files.
+ *
+ * @param archive - The capsule archive to check.
+ * @param assertion - The assertion with step label and expectations.
+ * @returns An {@link AssertionResult} indicating pass/fail with a reason.
+ *
+ * @example
+ * ```ts
+ * const result = checkAssertion(archive, {
+ * step: "loaded",
+ * expect: { url_contains: "example.com", dom_hash_stable: true },
+ * });
+ * ```
+ */
+export function checkAssertion(archive: CapsuleArchive, assertion: Assertion): AssertionResult {
+ const capsule = archive.manifest;
+ const step = capsule.steps.find((s) => s.label === assertion.step);
+
+ if (!step) {
+ return {
+ passed: false,
+ step: assertion.step,
+ message: `Step "${assertion.step}" not found in capsule`,
+ };
+ }
+
+ const failures: string[] = [];
+ const expect = assertion.expect;
+
+ // NOTE: url_contains checks the capsule's initial URL, not per-step URLs.
+ // Per-step URL tracking is not yet implemented in the capsule format.
+ if (expect.url_contains !== undefined) {
+ const url = capsule.initialState.url;
+ if (!url.includes(expect.url_contains)) {
+ failures.push(`url_contains "${expect.url_contains}" — got "${url}"`);
+ }
+ }
+
+ if (expect.initial_url_contains !== undefined) {
+ const url = capsule.initialState.url;
+ if (!url.includes(expect.initial_url_contains)) {
+ failures.push(`initial_url_contains "${expect.initial_url_contains}" — got "${url}"`);
+ }
+ }
+
+ if (expect.dom_hash_stable === true) {
+ const hash = step.observables.domSnapshotHash;
+ if (!hash || hash.length === 0) {
+ failures.push("dom_hash_stable — hash is empty");
+ }
+ }
+
+ if (expect.accessibility_contains !== undefined) {
+ const a11yPath = step.artifacts.accessibilityYaml;
+ const a11yBuffer = archive.files.get(a11yPath);
+ if (!a11yBuffer) {
+ failures.push(
+ `accessibility_contains "${expect.accessibility_contains}" — artifact file not found`
+ );
+ } else {
+ const content = a11yBuffer.toString("utf-8");
+ if (!content.includes(expect.accessibility_contains)) {
+ failures.push(
+ `accessibility_contains "${expect.accessibility_contains}" — not found in snapshot`
+ );
+ }
+ }
+ }
+
+ if (expect.network_count_gte !== undefined) {
+ const count = capsule.networkTranscript.entries.length;
+ if (count < expect.network_count_gte) {
+ failures.push(
+ `network_count_gte ${expect.network_count_gte} — got ${count}`
+ );
+ }
+ }
+
+ if (expect.network_count_lte !== undefined) {
+ const count = capsule.networkTranscript.entries.length;
+ if (count > expect.network_count_lte) {
+ failures.push(
+ `network_count_lte ${expect.network_count_lte} — got ${count}`
+ );
+ }
+ }
+
+ if (expect.screenshot_exists === true) {
+ const ssPath = step.artifacts.screenshot;
+ if (!archive.files.has(ssPath)) {
+ failures.push("screenshot_exists — screenshot artifact not found");
+ }
+ }
+
+ if (failures.length > 0) {
+ return {
+ passed: false,
+ step: assertion.step,
+ message: failures.join("; "),
+ };
+ }
+
+ return { passed: true, step: assertion.step };
+}
diff --git a/src/cli/replay.ts b/src/cli/replay.ts
new file mode 100644
index 0000000..2c0f9a1
--- /dev/null
+++ b/src/cli/replay.ts
@@ -0,0 +1,106 @@
+/**
+ * Replay command: load a capsule from disk, replay it in a browser,
+ * and output results (optionally with cost comparison).
+ */
+
+import { readFile } from "node:fs/promises";
+import { basename } from "node:path";
+import { deserializeCapsuleArchive } from "../capsule/builder.js";
+import { DBAR } from "../sdk.js";
+import { calculateCost, type CostBreakdown } from "./cost.js";
+
+/**
+ * Run a capsule replay and print results to stdout.
+ *
+ * @param capsulePath - Path to a serialized `.capsule` file on disk.
+ * @param options - CLI flags: `--cost` for cost comparison, `--json` for JSON output.
+ * @throws {Error} If the file cannot be read or the capsule is malformed.
+ */
+export async function runReplay(
+ capsulePath: string,
+ options: { cost?: boolean; json?: boolean }
+): Promise {
+ const raw = await readFile(capsulePath, "utf-8");
+ const archive = deserializeCapsuleArchive(raw);
+
+ // Dynamically import playwright-core to launch a browser for replay
+ const { chromium } = await import("playwright-core");
+ const browser = await chromium.launch();
+ const page = await browser.newPage();
+
+ try {
+ const result = await DBAR.replay(page, archive);
+ const capsule = archive.manifest;
+ const fileName = basename(capsulePath);
+
+ let costBreakdown: CostBreakdown | undefined;
+ if (options.cost) {
+ const domSnapshotSizes: number[] = [];
+ for (const step of capsule.steps) {
+ const domBuffer = archive.files.get(step.artifacts.domSnapshot);
+ domSnapshotSizes.push(domBuffer ? domBuffer.byteLength : 0);
+ }
+
+ costBreakdown = calculateCost({
+ networkRequestCount: capsule.networkTranscript.entries.length,
+ domSnapshotSizes,
+ stepCount: capsule.steps.length,
+ });
+ }
+
+ if (options.json) {
+ const output: Record = {
+ capsule: fileName,
+ steps: capsule.steps.length,
+ successCount: capsule.steps.length - result.divergences.length,
+ totalSteps: capsule.steps.length,
+ successRate: result.replaySuccessRate,
+ durationMs: result.overheadMs,
+ success: result.success,
+ divergences: result.divergences,
+ };
+ if (costBreakdown) {
+ output.cost = costBreakdown;
+ }
+ process.stdout.write(JSON.stringify(output, null, 2) + "\n");
+ } else {
+ const successCount =
+ capsule.steps.length -
+ new Set(result.divergences.map((d) => d.step)).size;
+ const pct =
+ capsule.steps.length > 0
+ ? ((successCount / capsule.steps.length) * 100).toFixed(0)
+ : "100";
+ const duration = (result.overheadMs / 1000).toFixed(2);
+
+ const lines: string[] = [
+ "DBAR Replay Results",
+ "\u2550".repeat(19),
+ `Capsule: ${fileName}`,
+ `Steps: ${capsule.steps.length}`,
+ `Success: ${successCount}/${capsule.steps.length} (${pct}%)`,
+ `Duration: ${duration}s`,
+ ];
+
+ if (costBreakdown) {
+ lines.push(
+ "",
+ "Cost Comparison",
+ "\u2500".repeat(15),
+ `Original run: $${costBreakdown.totalOriginalCost.toFixed(2)}`,
+ ` LLM tokens: $${costBreakdown.llmCost.toFixed(2)} (${costBreakdown.estimatedTokens.toLocaleString()} tokens)`,
+ ` Network: ${costBreakdown.networkRequestCount} requests`,
+ ` Compute: $${costBreakdown.computeCost.toFixed(2)}`,
+ "",
+ `Replay cost (API): $0.00`,
+ `Replay cost (compute): $${costBreakdown.replayComputeCost.toFixed(2)}`,
+ `API savings: $${costBreakdown.apiSavings.toFixed(2)} (${costBreakdown.apiSavingsPercent.toFixed(1)}%)`
+ );
+ }
+
+ process.stdout.write(lines.join("\n") + "\n");
+ }
+ } finally {
+ await browser.close();
+ }
+}
diff --git a/src/cli/run-eval.ts b/src/cli/run-eval.ts
new file mode 100644
index 0000000..dbece30
--- /dev/null
+++ b/src/cli/run-eval.ts
@@ -0,0 +1,186 @@
+/**
+ * Eval command runner: loads capsules from a directory and assertions from
+ * a YAML file, then checks each capsule against each assertion.
+ *
+ * YAML parsing uses a minimal subset parser (no external dep) that handles
+ * the simple assertions format. For production YAML, consider a real parser.
+ */
+
+import { readFile, readdir } from "node:fs/promises";
+import { join } from "node:path";
+import { deserializeCapsuleArchive } from "../capsule/builder.js";
+import { validateCapsule } from "../capsule/validator.js";
+import { checkAssertion, type Assertion } from "./eval.js";
+
+/** Result for a single capsule evaluated against all assertions. */
+interface CapsuleEvalResult {
+ file: string;
+ passed: number;
+ total: number;
+ failures: Array<{ step: string; message: string }>;
+}
+
+/**
+ * Parse a minimal YAML assertions file.
+ *
+ * Supports the specific format:
+ * ```yaml
+ * assertions:
+ * - step: "label"
+ * expect:
+ * key: value
+ * ```
+ *
+ * This is intentionally limited to avoid adding a YAML dependency.
+ * Boolean "true"/"false" and numeric values are coerced.
+ */
+function parseAssertionsYaml(content: string): Assertion[] {
+ const assertions: Assertion[] = [];
+ const lines = content.split("\n");
+
+ let current: { step?: string; expect: Record } | null = null;
+ let inExpect = false;
+
+ for (const rawLine of lines) {
+ const line = rawLine.trimEnd();
+
+ // Skip comments and empty lines
+ if (line.trim().startsWith("#") || line.trim() === "") continue;
+ // Skip the top-level "assertions:" key
+ if (line.trim() === "assertions:") continue;
+
+ // New assertion item
+ const stepMatch = line.match(/^\s+-\s+step:\s*"?([^"]*)"?\s*$/);
+ if (stepMatch) {
+ if (current?.step) {
+ assertions.push({ step: current.step, expect: current.expect as Assertion["expect"] });
+ }
+ current = { step: stepMatch[1]!.trim(), expect: {} };
+ inExpect = false;
+ continue;
+ }
+
+ // expect: block start
+ if (line.match(/^\s+expect:\s*$/)) {
+ inExpect = true;
+ continue;
+ }
+
+ // Key-value inside expect block
+ if (inExpect && current) {
+ const kvMatch = line.match(/^\s+([\w-]+):\s*"?([^"]*)"?\s*$/);
+ if (kvMatch) {
+ const key = kvMatch[1]!;
+ let value: unknown = kvMatch[2]!.trim();
+ // Coerce booleans and numbers
+ if (value === "true") value = true;
+ else if (value === "false") value = false;
+ else if (/^\d+(\.\d+)?$/.test(value as string)) value = Number(value);
+ current.expect[key] = value;
+ }
+ }
+ }
+
+ // Push the last assertion
+ if (current?.step) {
+ assertions.push({ step: current.step, expect: current.expect as Assertion["expect"] });
+ }
+
+ return assertions;
+}
+
+/**
+ * Run the eval command: load capsules, load assertions, check each combination.
+ *
+ * @param options - CLI options with paths to capsules directory and assertions YAML.
+ * @throws {Error} If the capsules directory or assertions file cannot be read.
+ */
+export async function runEval(
+ options: { capsules: string; assertions: string; json?: boolean }
+): Promise {
+ const assertionsContent = await readFile(options.assertions, "utf-8");
+ const assertions = parseAssertionsYaml(assertionsContent);
+
+ if (assertions.length === 0) {
+ process.stderr.write("Error: No assertions found in " + options.assertions + "\n");
+ process.exitCode = 1;
+ return;
+ }
+
+ const dirEntries = await readdir(options.capsules);
+ const capsuleFiles = dirEntries.filter((f) => f.endsWith(".capsule")).sort();
+
+ if (capsuleFiles.length === 0) {
+ process.stderr.write("Error: No .capsule files found in " + options.capsules + "\n");
+ process.exitCode = 1;
+ return;
+ }
+
+ const results: CapsuleEvalResult[] = [];
+
+ for (const file of capsuleFiles) {
+ const filePath = join(options.capsules, file);
+ const raw = await readFile(filePath, "utf-8");
+ const archive = deserializeCapsuleArchive(raw);
+
+ const validation = validateCapsule(archive);
+ if (!validation.valid) {
+ results.push({
+ file,
+ passed: 0,
+ total: assertions.length,
+ failures: [{ step: "*", message: `Capsule validation failed: ${validation.errors.map((e) => e.message).join(", ")}` }],
+ });
+ continue;
+ }
+
+ const failures: Array<{ step: string; message: string }> = [];
+ let passed = 0;
+
+ for (const assertion of assertions) {
+ const result = checkAssertion(archive, assertion);
+ if (result.passed) {
+ passed++;
+ } else {
+ failures.push({ step: assertion.step, message: result.message ?? "unknown failure" });
+ }
+ }
+
+ results.push({ file, passed, total: assertions.length, failures });
+ }
+
+ if (options.json) {
+ process.stdout.write(JSON.stringify({ capsules: results, assertions: assertions.length }, null, 2) + "\n");
+ return;
+ }
+
+ // Human-readable output
+ const totalCapsules = results.length;
+ const passedCapsules = results.filter((r) => r.failures.length === 0).length;
+ const failedCapsules = totalCapsules - passedCapsules;
+
+ const lines: string[] = [
+ "DBAR Eval Results",
+ "\u2550".repeat(17),
+ `Capsules: ${totalCapsules} | Assertions: ${assertions.length}`,
+ "",
+ ];
+
+ for (const result of results) {
+ if (result.failures.length === 0) {
+ lines.push(` ${result.file} \u2713 PASS (${result.passed}/${result.total})`);
+ } else {
+ lines.push(` ${result.file} \u2717 FAIL (${result.passed}/${result.total})`);
+ for (const f of result.failures) {
+ lines.push(` \u2717 step "${f.step}": ${f.message}`);
+ }
+ }
+ }
+
+ lines.push("", `Summary: ${passedCapsules}/${totalCapsules} passed, ${failedCapsules}/${totalCapsules} failed`);
+ process.stdout.write(lines.join("\n") + "\n");
+
+ if (failedCapsules > 0) {
+ process.exitCode = 1;
+ }
+}
diff --git a/src/coordinator.ts b/src/coordinator.ts
index 2a75735..95e6124 100644
--- a/src/coordinator.ts
+++ b/src/coordinator.ts
@@ -60,6 +60,8 @@ export interface CaptureSessionState {
>;
startTime: number;
aborted: boolean;
+ /** Whether the TimeVirtualizer has been started (deferred to first step). */
+ timeVirtualizerStarted: boolean;
}
/**
@@ -120,7 +122,10 @@ export class Coordinator {
});
await recorder.start();
- await timeVirtualizer.start();
+ // TimeVirtualizer is NOT started here — it's deferred to the first step()
+ // call. Starting virtual time during capture setup causes page.goto() with
+ // waitUntil:"networkidle" to hang because pauseIfNetworkFetchesPending
+ // pauses the browser's internal timers while Fetch events are pending.
cdpSession.on("disconnected" as any, () => {
trace.recordSession("cdp_session_lost");
@@ -145,6 +150,7 @@ export class Coordinator {
artifacts: new Map(),
startTime: Date.now(),
aborted: false,
+ timeVirtualizerStarted: false,
};
}
@@ -165,6 +171,13 @@ export class Coordinator {
const index = state.stepIndex;
state.trace.recordSession("step_start", { index, label });
+ // Start virtual time on first step (deferred from startCapture to avoid
+ // blocking page.goto with networkidle)
+ if (!state.timeVirtualizerStarted) {
+ await state.timeVirtualizer.start();
+ state.timeVirtualizerStarted = true;
+ }
+
// 1. Pause virtual time for deterministic snapshot
await state.timeVirtualizer.pause();
@@ -179,7 +192,7 @@ export class Coordinator {
// 3. Capture all observables in parallel
const [domResult, a11yResult, screenshotResult] = await Promise.all([
captureDOMSnapshot(state.cdpSession),
- captureAccessibilitySnapshot(state.page),
+ captureAccessibilitySnapshot(state.page, state.cdpSession),
captureScreenshot(state.page, { masks: state.options.screenshotMasks }),
]);
@@ -218,8 +231,8 @@ export class Coordinator {
state.trace.recordSnapshot(index, observables);
- // 7. Resume virtual time
- await state.timeVirtualizer.resume();
+ // 7. Suspend virtual time (advance mode) so navigation works between steps
+ await state.timeVirtualizer.suspend();
state.stepIndex++;
const captureMs = Date.now() - stepStart;
@@ -235,7 +248,9 @@ export class Coordinator {
state.aborted = true;
state.trace.recordSession("capture_abort");
await state.recorder.stop();
- await state.timeVirtualizer.stop();
+ if (state.timeVirtualizerStarted) {
+ await state.timeVirtualizer.stop();
+ }
}
/**
diff --git a/src/index.ts b/src/index.ts
index 651cfc1..35207c1 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -90,4 +90,4 @@ export { TraceTimeline, type TraceEntry } from "./telemetry/trace.js";
export { Coordinator, type CaptureOptions, type CaptureSessionState } from "./coordinator.js";
// SDK
-export { DBAR, CaptureSession, type ReplayOptions } from "./sdk.js";
+export { DBAR, CaptureSession, ReplaySession, type ReplayOptions, type ReplayStepResult } from "./sdk.js";
diff --git a/src/sdk.ts b/src/sdk.ts
index 2c0fa8d..8b6b1bc 100644
--- a/src/sdk.ts
+++ b/src/sdk.ts
@@ -6,6 +6,8 @@ import type {
ValidationResult,
Divergence,
StepObservables,
+ NetworkTranscript,
+ NetworkEntry,
} from "./capsule/types.js";
import {
buildCapsule,
@@ -24,6 +26,41 @@ import { captureScreenshot } from "./snapshot/screenshot.js";
import { restoreStorageState } from "./snapshot/state.js";
import { TraceTimeline } from "./telemetry/trace.js";
+// ---------------------------------------------------------------------------
+// Transcript hydration — resolve deduplicated body paths to base64 content
+// ---------------------------------------------------------------------------
+
+/**
+ * buildCapsule deduplicates response bodies into `network/` files and
+ * replaces `entry.response.body` with the file path. Before replay, we need
+ * to resolve those paths back to actual base64 content so the NetworkReplayer
+ * can serve them via Fetch.fulfillRequest.
+ */
+function hydrateTranscript(
+ transcript: NetworkTranscript,
+ files: Map
+): NetworkTranscript {
+ const entries: NetworkEntry[] = transcript.entries.map((entry) => {
+ if (!entry.response) return entry;
+
+ const bodyRef = entry.response.body;
+ // If the body looks like a file path (network/), resolve it
+ if (bodyRef.startsWith("network/") && files.has(bodyRef)) {
+ const bodyBuffer = files.get(bodyRef)!;
+ return {
+ ...entry,
+ response: {
+ ...entry.response,
+ body: bodyBuffer.toString("base64"),
+ },
+ };
+ }
+ return entry;
+ });
+
+ return { orderingPolicy: transcript.orderingPolicy, entries };
+}
+
// ---------------------------------------------------------------------------
// Capture Session
// ---------------------------------------------------------------------------
@@ -83,7 +120,9 @@ export class CaptureSession {
// Stop subsystems
await this.state.recorder.stop();
- await this.state.timeVirtualizer.stop();
+ if (this.state.timeVirtualizerStarted) {
+ await this.state.timeVirtualizer.stop();
+ }
this.state.trace.recordSession("capture_finish");
// Get recorded network transcript
@@ -120,7 +159,7 @@ export class CaptureSession {
// Replay Options
// ---------------------------------------------------------------------------
-/** Options for {@link DBAR.replay}. */
+/** Options for {@link DBAR.replay} and {@link DBAR.startReplay}. */
export interface ReplayOptions {
/** Policy for requests not found in the transcript (default: "block"). */
unmatchedRequestPolicy?: "block" | "continue";
@@ -132,6 +171,224 @@ export interface ReplayOptions {
compareScreenshots?: boolean;
}
+// ---------------------------------------------------------------------------
+// Replay Session (step-by-step replay for multi-step capsules)
+// ---------------------------------------------------------------------------
+
+/** Per-step result returned by {@link ReplaySession.step}. */
+export interface ReplayStepResult {
+ index: number;
+ matched: boolean;
+ divergences: Divergence[];
+ liveObservables: StepObservables;
+ expectedObservables: StepObservables;
+}
+
+/**
+ * Internal state for an active replay session.
+ * @internal
+ */
+interface ReplaySessionState {
+ page: Page;
+ cdpSession: CDPSession;
+ archive: CapsuleArchive;
+ options: ReplayOptions;
+ replayer: NetworkReplayer;
+ timeVirtualizer: TimeVirtualizer;
+ timeVirtualizerStarted: boolean;
+ trace: TraceTimeline;
+ divergences: Divergence[];
+ stepIndex: number;
+ matchedSteps: number;
+ timeToDivergence: number | undefined;
+ startTime: number;
+}
+
+/**
+ * A step-by-step replay session for multi-step capsules.
+ *
+ * Unlike {@link DBAR.replay} which runs all steps automatically, a
+ * ReplaySession lets the caller interleave user actions (navigation,
+ * clicks) between step comparisons — matching what happened during capture.
+ *
+ * @example
+ * ```ts
+ * const rs = await DBAR.startReplay(page, archive);
+ * await page.goto("https://example.com");
+ * const r0 = await rs.step(); // compares against capsule step 0
+ * await page.click("a.nav");
+ * const r1 = await rs.step(); // compares against capsule step 1
+ * const result = await rs.finish();
+ * ```
+ */
+export class ReplaySession {
+ private state: ReplaySessionState;
+ private finished = false;
+
+ /** @internal — use {@link DBAR.startReplay} to create. */
+ constructor(state: ReplaySessionState) {
+ this.state = state;
+ }
+
+ /** Number of steps compared so far. */
+ get stepCount(): number {
+ return this.state.stepIndex;
+ }
+
+ /** Total steps in the capsule. */
+ get totalSteps(): number {
+ return this.state.archive.manifest.steps.length;
+ }
+
+ /**
+ * Compare the current live page state against the next expected capsule step.
+ * Call this after performing the same user action that preceded this step
+ * during capture.
+ *
+ * @returns Per-step comparison result
+ */
+ async step(): Promise {
+ if (this.finished) throw new Error("Replay session already finished");
+ const { state } = this;
+ const capsule = state.archive.manifest;
+ const expectedStep = capsule.steps[state.stepIndex];
+ if (!expectedStep) throw new Error(`No more steps in capsule (have ${capsule.steps.length})`);
+
+ const stepDivergences: Divergence[] = [];
+
+ state.replayer.setStepIndex(expectedStep.index);
+
+ // Start virtual time on first step (deferred from startReplay to avoid
+ // blocking page.goto with networkidle)
+ if (!state.timeVirtualizerStarted) {
+ await state.timeVirtualizer.start();
+ state.timeVirtualizerStarted = true;
+ }
+
+ // Pause time, wait for quiescence
+ await state.timeVirtualizer.pause();
+ const { quiescent } = await state.timeVirtualizer.waitForQuiescence();
+ if (!quiescent) {
+ const d: Divergence = { step: expectedStep.index, type: "quiescence_timeout" };
+ stepDivergences.push(d);
+ state.divergences.push(d);
+ }
+
+ // Capture live observables
+ const [domResult, a11yResult, screenshotResult] = await Promise.all([
+ captureDOMSnapshot(state.cdpSession),
+ captureAccessibilitySnapshot(state.page, state.cdpSession),
+ captureScreenshot(state.page, { masks: state.options.screenshotMasks }),
+ ]);
+
+ const liveObservables: StepObservables = {
+ domSnapshotHash: domResult.hash,
+ accessibilityHash: a11yResult.hash,
+ screenshotHash: screenshotResult.hash,
+ networkDigest: expectedStep.observables.networkDigest,
+ };
+
+ // Compare observables
+ let stepDiverged = false;
+
+ if (liveObservables.domSnapshotHash !== expectedStep.observables.domSnapshotHash) {
+ stepDiverged = true;
+ const d: Divergence = {
+ step: expectedStep.index,
+ type: "dom_mismatch",
+ expected: expectedStep.observables.domSnapshotHash,
+ actual: liveObservables.domSnapshotHash,
+ };
+ stepDivergences.push(d);
+ state.divergences.push(d);
+ }
+
+ if (liveObservables.accessibilityHash !== expectedStep.observables.accessibilityHash) {
+ stepDiverged = true;
+ const d: Divergence = {
+ step: expectedStep.index,
+ type: "accessibility_mismatch",
+ expected: expectedStep.observables.accessibilityHash,
+ actual: liveObservables.accessibilityHash,
+ };
+ stepDivergences.push(d);
+ state.divergences.push(d);
+ }
+
+ if (
+ state.options.compareScreenshots &&
+ liveObservables.screenshotHash !== expectedStep.observables.screenshotHash
+ ) {
+ const d: Divergence = {
+ step: expectedStep.index,
+ type: "screenshot_mismatch",
+ details: "screenshot hash mismatch (advisory)",
+ expected: expectedStep.observables.screenshotHash,
+ actual: liveObservables.screenshotHash,
+ };
+ stepDivergences.push(d);
+ state.divergences.push(d);
+ }
+
+ if (!stepDiverged) {
+ state.matchedSteps++;
+ } else if (state.timeToDivergence === undefined) {
+ state.timeToDivergence = expectedStep.index;
+ }
+
+ state.trace.recordSnapshot(expectedStep.index, liveObservables);
+
+ // Suspend virtual time (advance mode) so navigation works between steps
+ await state.timeVirtualizer.suspend();
+ state.stepIndex++;
+
+ return {
+ index: expectedStep.index,
+ matched: !stepDiverged,
+ divergences: stepDivergences,
+ liveObservables,
+ expectedObservables: expectedStep.observables,
+ };
+ }
+
+ /**
+ * Finish the replay session and compute final metrics.
+ * Must be called after all steps have been compared (or early if aborting).
+ */
+ async finish(): Promise {
+ if (this.finished) throw new Error("Replay session already finished");
+ this.finished = true;
+ const { state } = this;
+
+ // Collect replayer divergences
+ const replayerDivergences = state.replayer.getDivergences();
+ if (state.timeToDivergence === undefined && replayerDivergences.length > 0) {
+ state.timeToDivergence = replayerDivergences[0]!.step;
+ }
+
+ // Cleanup
+ await state.replayer.stop();
+ if (state.timeVirtualizerStarted) {
+ await state.timeVirtualizer.stop();
+ }
+ state.trace.recordSession("replay_finish");
+
+ // Compute metrics
+ const totalSteps = state.archive.manifest.steps.length;
+ const replaySuccessRate = totalSteps > 0 ? state.matchedSteps / totalSteps : 1;
+ const determinismViolationRate = totalSteps > 0 ? 1 - replaySuccessRate : 0;
+
+ return {
+ success: state.divergences.length === 0,
+ replaySuccessRate,
+ determinismViolationRate,
+ timeToDivergence: state.timeToDivergence,
+ divergences: state.divergences,
+ overheadMs: Date.now() - state.startTime,
+ };
+ }
+}
+
// ---------------------------------------------------------------------------
// DBAR Static API
// ---------------------------------------------------------------------------
@@ -145,15 +402,18 @@ export interface ReplayOptions {
* ```ts
* // Capture
* const session = await DBAR.capture(page);
+ * await page.goto("https://example.com");
* await session.step("step-0");
* const archive = await session.finish();
*
- * // Validate
- * const validation = DBAR.validate(archive);
+ * // Step-by-step replay (for multi-step capsules)
+ * const rs = await DBAR.startReplay(replayPage, archive);
+ * await replayPage.goto("https://example.com");
+ * const r0 = await rs.step();
+ * const result = await rs.finish();
*
- * // Replay
- * const result = await DBAR.replay(page, archive);
- * console.log(result.replaySuccessRate); // 1.0
+ * // Auto-replay (single-step capsules only)
+ * const result2 = await DBAR.replay(page, archive);
* ```
*/
export class DBAR {
@@ -174,15 +434,74 @@ export class DBAR {
}
/**
- * Replay a captured session against a live browser page, comparing
- * observables at each step to detect determinism divergences.
+ * Start a step-by-step replay session. Use this for multi-step capsules
+ * where you need to interleave user actions between step comparisons.
+ *
+ * Sets up network interception and virtual time BEFORE restoring initial
+ * state, so the initial page load is served from the recorded transcript.
+ *
+ * @param page - Playwright Page for replay (should be a fresh page)
+ * @param archive - The capsule archive to replay
+ * @param options - Replay configuration
+ * @returns A {@link ReplaySession} with step()/finish() methods
+ */
+ static async startReplay(
+ page: Page,
+ archive: CapsuleArchive,
+ options: ReplayOptions = {}
+ ): Promise {
+ const capsule = archive.manifest;
+ const divergences: Divergence[] = [];
+ const trace = new TraceTimeline();
+ trace.recordSession("replay_start", { capsuleId: capsule.id });
+
+ // Set up CDP session + subsystems BEFORE initial navigation
+ const cdpSession: CDPSession = await page.context().newCDPSession(page);
+
+ const timeVirtualizer = new TimeVirtualizer(cdpSession, {
+ stepBudgetMs: options.stepBudgetMs ?? 10000,
+ initialVirtualTime: capsule.seeds.initialTime,
+ });
+
+ // Hydrate transcript: resolve deduplicated body paths to actual base64
+ const hydrated = hydrateTranscript(capsule.networkTranscript, archive.files);
+
+ const replayer = new NetworkReplayer(cdpSession, hydrated, {
+ unmatchedRequestPolicy: options.unmatchedRequestPolicy ?? "block",
+ onDivergence: (d) => divergences.push(d),
+ onFetchResolved: () => timeVirtualizer.trackFetchResolution(),
+ });
+
+ // Start network interception (but NOT virtual time — that's deferred to
+ // first step() to avoid blocking page.goto with networkidle)
+ await replayer.start();
+
+ // Restore cookies/localStorage then navigate to initial URL
+ await restoreStorageState(page, capsule.initialState);
+
+ return new ReplaySession({
+ page,
+ cdpSession,
+ archive,
+ options,
+ replayer,
+ timeVirtualizer,
+ timeVirtualizerStarted: false,
+ trace,
+ divergences,
+ stepIndex: 0,
+ matchedSteps: 0,
+ timeToDivergence: undefined,
+ startTime: Date.now(),
+ });
+ }
+
+ /**
+ * Auto-replay a captured session, comparing all steps sequentially.
*
- * The replayer:
- * 1. Restores initial state (cookies, localStorage, navigation)
- * 2. Sets up network interception from the capsule transcript
- * 3. Starts virtual time control
- * 4. Re-executes each step boundary, comparing observable hashes
- * 5. Computes RSR, DVR, and TTD metrics
+ * **Note:** This method does not replay user actions between steps. It works
+ * well for single-step capsules. For multi-step capsules, use
+ * {@link DBAR.startReplay} to interleave actions between step comparisons.
*
* @param page - Playwright Page for replay (should be a fresh page)
* @param archive - The capsule archive to replay
@@ -194,16 +513,14 @@ export class DBAR {
archive: CapsuleArchive,
options: ReplayOptions = {}
): Promise {
- const startTime = Date.now();
const capsule = archive.manifest;
const divergences: Divergence[] = [];
const trace = new TraceTimeline();
+ const startTime = Date.now();
trace.recordSession("replay_start", { capsuleId: capsule.id });
- // 1. Restore initial state
- await restoreStorageState(page, capsule.initialState);
-
- // 2. Set up CDP session + subsystems
+ // Set up CDP + subsystems BEFORE navigation (fix: interception must be
+ // active when restoreStorageState navigates to the initial URL)
const cdpSession: CDPSession = await page.context().newCDPSession(page);
const timeVirtualizer = new TimeVirtualizer(cdpSession, {
@@ -211,33 +528,41 @@ export class DBAR {
initialVirtualTime: capsule.seeds.initialTime,
});
- const replayer = new NetworkReplayer(cdpSession, capsule.networkTranscript, {
+ const hydrated = hydrateTranscript(capsule.networkTranscript, archive.files);
+
+ const replayer = new NetworkReplayer(cdpSession, hydrated, {
unmatchedRequestPolicy: options.unmatchedRequestPolicy ?? "block",
onDivergence: (d) => divergences.push(d),
onFetchResolved: () => timeVirtualizer.trackFetchResolution(),
});
await replayer.start();
- await timeVirtualizer.start();
+ // TimeVirtualizer deferred to first step (same as capture)
+
+ // Restore initial state (navigation goes through replayer's transcript)
+ await restoreStorageState(page, capsule.initialState);
- // 3. Replay each step
+ // Replay each step
let matchedSteps = 0;
let timeToDivergence: number | undefined;
+ let tvStarted = false;
for (const expectedStep of capsule.steps) {
replayer.setStepIndex(expectedStep.index);
- // Pause time, wait for quiescence
+ if (!tvStarted) {
+ await timeVirtualizer.start();
+ tvStarted = true;
+ }
await timeVirtualizer.pause();
const { quiescent } = await timeVirtualizer.waitForQuiescence();
if (!quiescent) {
divergences.push({ step: expectedStep.index, type: "quiescence_timeout" });
}
- // Capture live observables
const [domResult, a11yResult, screenshotResult] = await Promise.all([
captureDOMSnapshot(cdpSession),
- captureAccessibilitySnapshot(page),
+ captureAccessibilitySnapshot(page, cdpSession),
captureScreenshot(page, { masks: options.screenshotMasks }),
]);
@@ -245,12 +570,9 @@ export class DBAR {
domSnapshotHash: domResult.hash,
accessibilityHash: a11yResult.hash,
screenshotHash: screenshotResult.hash,
- // Network digest is not recomputed during replay — divergences are
- // detected at the request level by the NetworkReplayer.
networkDigest: expectedStep.observables.networkDigest,
};
- // Compare observables (per DBAR-C1: dom, accessibility, networkDigest are strict)
let stepDiverged = false;
if (liveObservables.domSnapshotHash !== expectedStep.observables.domSnapshotHash) {
@@ -273,16 +595,14 @@ export class DBAR {
});
}
- // Screenshot comparison is opt-in (v1 captures but does not compare by default).
- // Advisory only — does not affect stepDiverged.
if (
options.compareScreenshots &&
liveObservables.screenshotHash !== expectedStep.observables.screenshotHash
) {
divergences.push({
step: expectedStep.index,
- type: "dom_mismatch",
- details: "screenshot hash mismatch (advisory, not a strict divergence)",
+ type: "screenshot_mismatch",
+ details: "screenshot hash mismatch (advisory)",
expected: expectedStep.observables.screenshotHash,
actual: liveObservables.screenshotHash,
});
@@ -295,25 +615,20 @@ export class DBAR {
}
trace.recordSnapshot(expectedStep.index, liveObservables);
-
- // Resume time for next step
- await timeVirtualizer.resume();
+ await timeVirtualizer.suspend();
}
- // 4. Collect replayer divergences (unmatched requests)
const replayerDivergences = replayer.getDivergences();
- // Replayer divergences are already pushed via onDivergence callback,
- // but update TTD if an unmatched request was the first divergence
if (timeToDivergence === undefined && replayerDivergences.length > 0) {
timeToDivergence = replayerDivergences[0]!.step;
}
- // 5. Cleanup
await replayer.stop();
- await timeVirtualizer.stop();
+ if (tvStarted) {
+ await timeVirtualizer.stop();
+ }
trace.recordSession("replay_finish");
- // 6. Compute metrics
const totalSteps = capsule.steps.length;
const replaySuccessRate = totalSteps > 0 ? matchedSteps / totalSteps : 1;
const determinismViolationRate = totalSteps > 0 ? 1 - replaySuccessRate : 0;
diff --git a/src/snapshot/accessibility.ts b/src/snapshot/accessibility.ts
index a02c5c7..42f4697 100644
--- a/src/snapshot/accessibility.ts
+++ b/src/snapshot/accessibility.ts
@@ -1,5 +1,5 @@
import { createHash } from "node:crypto";
-import type { Page } from "playwright-core";
+import type { Page, CDPSession } from "playwright-core";
/** Result of an accessibility tree snapshot with a deterministic hash. */
export interface AccessibilitySnapshotResult {
@@ -9,25 +9,93 @@ export interface AccessibilitySnapshotResult {
}
/**
- * Capture the page's accessibility tree via Playwright and produce a
- * SHA-256 hash of the canonicalized JSON for determinism comparison.
+ * Capture the page's accessibility tree and produce a SHA-256 hash of the
+ * canonicalized JSON for determinism comparison.
+ *
+ * Tries three strategies in order:
+ * 1. Playwright's `page.accessibility.snapshot()` (removed in Playwright >= 1.49)
+ * 2. CDP `Accessibility.getFullAXTree` via an active CDP session
+ * 3. Playwright's `page.locator("body").ariaSnapshot()` as a last resort
*
* @param page - Playwright Page instance
+ * @param cdpSession - Optional CDP session for the CDP fallback
* @returns The accessibility tree, its canonical JSON string, and SHA-256 hash
*/
export async function captureAccessibilitySnapshot(
- page: Page
+ page: Page,
+ cdpSession?: CDPSession
): Promise {
- const tree = await (page as any).accessibility.snapshot({
- interestingOnly: false,
- });
+ let tree: unknown;
+ let hashSource: unknown;
- const serialized = canonicalizeTree(tree);
+ // Strategy 1: legacy Playwright API (available in Playwright < 1.49)
+ const legacyA11y = (page as any).accessibility;
+ if (legacyA11y && typeof legacyA11y.snapshot === "function") {
+ try {
+ tree = await legacyA11y.snapshot({ interestingOnly: false });
+ hashSource = tree;
+ } catch {
+ // Might exist but throw at runtime — fall through
+ }
+ }
+
+ // Strategy 2: CDP Accessibility.getFullAXTree — extract structural fields only
+ if (tree == null && cdpSession) {
+ try {
+ const result = await cdpSession.send("Accessibility.getFullAXTree" as any);
+ const nodes = (result as any).nodes as any[];
+ tree = nodes;
+ // Strip non-deterministic fields (nodeId, backendDOMNodeId, frameId,
+ // parentId, childIds, chromeRole) — keep only structural content
+ hashSource = nodes.map((n: any) => ({
+ role: n.role?.value,
+ name: n.name?.value,
+ value: n.value?.value,
+ description: n.description?.value,
+ properties: (n.properties as any[] | undefined)
+ ?.filter((p: any) => !NON_DETERMINISTIC_AX_PROPS.has(p.name))
+ ?.map((p: any) => ({ name: p.name, value: p.value?.value })),
+ }));
+ } catch {
+ // CDP method might not be available — fall through
+ }
+ }
+
+ // Strategy 3: Playwright ariaSnapshot (string-based, wrap in object for hashing)
+ if (tree == null) {
+ try {
+ const ariaText: string = await page.locator("body").ariaSnapshot();
+ tree = {
+ role: "WebArea",
+ name: await page.title(),
+ ariaSnapshot: ariaText,
+ };
+ hashSource = tree;
+ } catch {
+ tree = { role: "WebArea", name: "page", children: [] };
+ hashSource = tree;
+ }
+ }
+
+ const serialized = canonicalizeTree(hashSource);
const hash = createHash("sha256").update(serialized).digest("hex");
return { tree, hash, serialized };
}
+/** AX node properties that vary between runs and should be excluded from hash. */
+const NON_DETERMINISTIC_AX_PROPS = new Set([
+ "focused",
+ "focusable",
+ "settable",
+ "editable",
+ "live",
+ "atomic",
+ "relevant",
+ "busy",
+ "root",
+]);
+
/**
* Sort object keys recursively for deterministic JSON serialization.
* Arrays preserve their element order; only object key order is normalized.
diff --git a/src/snapshot/dom.ts b/src/snapshot/dom.ts
index 6ea4dc3..ca47640 100644
--- a/src/snapshot/dom.ts
+++ b/src/snapshot/dom.ts
@@ -1,7 +1,7 @@
import { createHash } from "node:crypto";
import type { CDPSession } from "playwright-core";
-/** Result of a CDP DOM snapshot capture with a deterministic hash. */
+/** Result of a DOM snapshot capture with a deterministic hash. */
export interface DOMSnapshotResult {
snapshot: unknown;
hash: string;
@@ -9,27 +9,47 @@ export interface DOMSnapshotResult {
}
/**
- * Capture a DOM snapshot via CDP `DOMSnapshot.captureSnapshot` and produce
- * a SHA-256 hash of the canonicalized JSON for determinism comparison.
+ * Capture a DOM snapshot and produce a SHA-256 hash for determinism comparison.
+ *
+ * The hash is computed from the page's serialized HTML (`document.documentElement.outerHTML`)
+ * which is structural and layout-independent — it won't vary due to rendering
+ * differences (pixel positions, font metrics, paint order) between runs.
+ *
+ * A full CDP `DOMSnapshot.captureSnapshot` is also captured as the artifact
+ * for debugging and analysis, but it is NOT used for the hash.
*
* @param cdpSession - Active CDP session to the target page
- * @returns Snapshot data, its canonical JSON string, and SHA-256 hash
+ * @returns Snapshot data, the serialized HTML, and SHA-256 hash
*/
-export async function captureDOMSnapshot(cdpSession: CDPSession): Promise {
- await cdpSession.send("DOMSnapshot.enable" as any);
+export async function captureDOMSnapshot(
+ cdpSession: CDPSession
+): Promise {
+ // Get the structural HTML via CDP DOM.getOuterHTML — this is deterministic
+ // for identical page content (unlike DOMSnapshot.captureSnapshot which
+ // includes computed styles and layout data that vary between runs).
+ const { root } = (await cdpSession.send("DOM.getDocument" as any, {
+ depth: 0,
+ })) as { root: { nodeId: number } };
+
+ const { outerHTML } = (await cdpSession.send("DOM.getOuterHTML" as any, {
+ nodeId: root.nodeId,
+ })) as { outerHTML: string };
+
+ // The outerHTML is the canonical structural representation of the DOM.
+ const serialized = outerHTML;
+ const hash = createHash("sha256").update(serialized).digest("hex");
+ // Also capture the full CDP snapshot as the artifact for debugging/analysis.
+ await cdpSession.send("DOMSnapshot.enable" as any);
const snapshot = await cdpSession.send(
"DOMSnapshot.captureSnapshot" as any,
{
computedStyles: ["display", "visibility", "opacity", "position"],
includePaintOrder: false,
- includeDOMRects: true,
+ includeDOMRects: false,
} as any
);
- // Canonicalize by JSON-serializing with sorted keys
- const serialized = JSON.stringify(snapshot, Object.keys(snapshot as object).sort());
- const hash = createHash("sha256").update(serialized).digest("hex");
-
return { snapshot, hash, serialized };
}
+
diff --git a/src/time/virtualizer.ts b/src/time/virtualizer.ts
index 5fbd05c..ae0329f 100644
--- a/src/time/virtualizer.ts
+++ b/src/time/virtualizer.ts
@@ -106,6 +106,19 @@ export class TimeVirtualizer {
await this.setPolicy("pauseIfNetworkFetchesPending");
}
+ /**
+ * Suspend virtual time control — lets time advance normally with no budget.
+ * Use this between step boundaries so that page.goto() and
+ * waitForLoadState("networkidle") work without hanging.
+ */
+ async suspend(): Promise {
+ this.currentPolicy = "advance";
+ // Send "advance" WITHOUT a budget so virtual time runs indefinitely
+ // (unlike setPolicy which always adds stepBudgetMs for non-pause policies)
+ const params: Record = { policy: "advance" };
+ await this.cdpSession.send("Emulation.setVirtualTimePolicy" as any, params as any);
+ }
+
/**
* Wait for network quiescence or timeout.
* @returns `{ quiescent: true }` if all network activity settled,
diff --git a/tsup.config.ts b/tsup.config.ts
index 6eb7da2..2fcb0f6 100644
--- a/tsup.config.ts
+++ b/tsup.config.ts
@@ -3,6 +3,7 @@ import { defineConfig } from "tsup";
export default defineConfig({
entry: {
index: "src/index.ts",
+ cli: "src/cli.ts",
},
format: ["cjs", "esm"],
dts: true,