@divlet/core 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/AGENTS.md +3 -2
  2. package/BACKLOG.md +3 -4
  3. package/CHANGELOG.md +11 -2
  4. package/README.md +16 -6
  5. package/dist/core/lib/acceptance.d.ts +18 -0
  6. package/dist/core/lib/acceptance.js +29 -0
  7. package/dist/core/lib/assets.js +1 -1
  8. package/dist/core/lib/browser-board.d.ts +41 -0
  9. package/dist/core/lib/browser-board.js +203 -0
  10. package/dist/core/lib/browser-mcp.d.ts +131 -0
  11. package/dist/core/lib/browser-mcp.js +448 -0
  12. package/dist/core/lib/browser-media-type.d.ts +4 -0
  13. package/dist/core/lib/browser-media-type.js +7 -0
  14. package/dist/core/lib/browser-runtime.d.ts +598 -0
  15. package/dist/core/lib/browser-runtime.js +674 -0
  16. package/dist/core/lib/codec.js +2 -1
  17. package/dist/core/lib/contract.d.ts +1 -152
  18. package/dist/core/lib/contract.js +3 -359
  19. package/dist/core/lib/diff.js +1 -1
  20. package/dist/core/lib/prepare.d.ts +27 -0
  21. package/dist/core/lib/prepare.js +41 -0
  22. package/dist/core/lib/project.js +3 -3
  23. package/dist/core/lib/render.d.ts +2 -9
  24. package/dist/core/lib/render.js +10 -35
  25. package/dist/core/lib/runtime.d.ts +1 -1
  26. package/dist/core/lib/runtime.js +1 -21
  27. package/dist/core/lib/tools.d.ts +154 -0
  28. package/dist/core/lib/tools.js +364 -0
  29. package/dist/core/lib/validation.d.ts +5 -0
  30. package/dist/core/lib/validation.js +21 -0
  31. package/dist/skills/divlet/SKILL.md +7 -1
  32. package/docs/README.md +1 -0
  33. package/docs/agents.md +11 -1
  34. package/docs/architecture.md +15 -7
  35. package/docs/board.md +1 -1
  36. package/docs/browser-mcp.md +108 -0
  37. package/docs/browser-runtime.md +6 -4
  38. package/docs/publishing.md +6 -6
  39. package/docs/testing.md +8 -1
  40. package/examples/README.md +2 -2
  41. package/examples/landing/landing.divlet +1 -1
  42. package/package.json +24 -5
  43. package/skills/divlet/SKILL.md +7 -1
package/AGENTS.md CHANGED
@@ -45,17 +45,18 @@ divlet is a local browser layout board for agents. It edits validated static HTM
45
45
  - **Embedded-resource target:** Project-owned file-asset bytes belong in the native `.divlet`, with only disposable materialization under `.divlet/<project>/assets/` beside generated pages. A canonical sibling `assets/` folder or a cache-only copy is not the target. The declared hierarchical `assets` tree owns reusable names/paths; explicit references materialize those files, while literal Data URLs at use sites remain inline. Never automatically extract inline URLs or rename declared keys merely to deduplicate bytes. Implement storage/reference validation, byte budgets and all resource consumers coherently before claiming this capability; preserve existing resources until conversion retains their bytes. Preserve the existing bounded local-resource import model; resource identifiers never authorize executable content or unrequested network fetches.
46
46
  - **Public demo data:** Never use `runtime.info()` as a public snapshot template: it includes private paths and session/editor state. Project inspection data only from explicitly curated public prepared content, with an allowlisted state shape and deliberate presentation geometry. Keep prepared HTML as rendering authority; metadata omission/model validity do not establish provenance, content secrecy, reference closure or whole-UI read-only enforcement.
47
47
  - **One authored landing:** Each example owns its folder. The canonical English onboarding/demo source belongs at `examples/landing/landing.divlet`; its `index` frame generates `examples/landing/.divlet/landing/index.html`. Keep identity, commands and release status consistent with the root README. Do not maintain a second hand-authored HTML site or marketing tree in core code. Other bundled examples may remain importer/site/layout scenarios, not duplicate landing sources. Any composition that seeds workspaces from this template must create an independently owned validated copy, never edit the installed master; explicit paths and no-demo creation remain authoritative.
48
- - **The landing build is derived, tracked and selectively published:** the named `examples/landing/.divlet/landing/` build is an explicit exception to ignoring generated project output, not permission to hand-edit it or commit arbitrary caches. Regenerate through normal validation and output ownership; a fresh checkout must rebuild safely, with portable provenance where required. Never track locks, recovery trash or session capabilities. CI publishes the prepared portable HTML/CSS/resource set to the `gh-pages` root, not a raw cache-directory copy: sidecars, ownership metadata, trash and editor/private payloads are not public artifacts. `scripts/landing-hash.mjs` owns both the published file set and its fingerprint; unchanged public bytes mean no deployment commit. Check source/build freshness in a relocated sandbox, preserve self-contained rendering and relative URLs, and do not mistake this architecture for authorization to push or deploy. Keep the canonical source/assets in the npm example payload, but exclude this cache; the Pages public set adds only its required `.nojekyll` marker and fingerprint record, not editor metadata.
48
+ - **The landing build is derived, tracked and selectively published:** the named `examples/landing/.divlet/landing/` build is an explicit exception to ignoring generated project output, not permission to hand-edit it or commit arbitrary caches. Regenerate through normal validation and output ownership; a fresh checkout must rebuild safely, with portable provenance where required. Never track locks, recovery trash or session capabilities. CI publishes the prepared portable HTML/CSS/resource set to the `gh-pages` root, not a raw cache-directory copy: sidecars, ownership metadata, trash and editor/private payloads are not public artifacts. `scripts/pages-build.mjs` owns the Pages set (the static board plus this page under `page/`) and its fingerprint, and `scripts/landing-hash.mjs` owns the prepared page's subset; unchanged public bytes mean no deployment commit. Check source/build freshness in a relocated sandbox, preserve self-contained rendering and relative URLs, and do not mistake this architecture for authorization to push or deploy. Keep the canonical source/assets in the npm example payload, but exclude this cache; the Pages public set adds the static board's own files, its `.nojekyll` marker and fingerprint record, never editor metadata or private payloads.
49
49
  - **The inspector is one vertical flow:** Section summaries are the only section-navigation controls; do not duplicate them in horizontal tabs, floating jump bars or reorder strips. Keep sections in their owning panels' stable order and remember open/closed state as a local preference, not project content. Text alignment belongs beside text colour, size and weight in Appearance; Layout owns container and item alignment. The read-only measured X/Y/W/H and overflow block is the final section, after metadata. Use native keyboard-accessible disclosure controls, unobstructed vertical scrolling and whole label/control units that reflow at narrow widths; minimal chrome must not remove access to properties.
50
50
  - **A block header names the block:** A section's caption says what the section is ("Artboards", "Elements"), not which item is selected — the selection is already lit in the list or tree it belongs to, and repeating its name costs the width a narrow panel needs. Detail about the current choice belongs on the row, in a tooltip, or in the empty state, never in the header.
51
51
  - **One slot, one occupant:** When a host can do something another cannot, the control replaces the affordance it stands in for rather than stacking beside it — the composer's footer is either the key hints (a host that can only answer prompts) or the model/thinking selectors (a host that owns the session), never both. Pi users are assumed to know the shortcuts; the selectors carry their tips, and `?` still lists every key. Send remains the footer's bottom-right action when either occupant wraps: use its own auto margin and bottom alignment, not a preceding flex spacer.
52
52
  - **Entry points say what they are:** The root `@divlet/core` package's entry is the product library, with named public subpaths such as `./runtime`, `./contract`, `./model`, `./boards`, `./plugins`, `./engine` and the shared `./skill` resource. It has no bins, Pi manifest/peer requirements or `./pi` export. Each adapter package owns its `dist/index.js` entry, exports and relevant bin or `pi.extensions`; host-specific keywords and dependencies stay there. A folder-scanning Pi installation targets the built `pi` package, never the repository root. Source execution uses the `divlet-source` export condition; `scripts/source.mjs` propagates it through `NODE_OPTIONS` to children. The npm workspace includes `.`, `web`, `cli`, `mcp`, `pi` and `plugins/*`. The root remains the `@divlet/core` library manifest, not a private wrapper. `web/package.json` owns `@divlet/web` directly; its DOM-only build compiles public ports and copies native browser assets only to `web/dist/`, never into the core tarball. Development runs sources; `dist/` is publishing output only.
53
+ - **Browser-hosted MCP:** a static site may be its own MCP provider ([browser-hosted MCP](docs/browser-mcp.md)). `@divlet/web/mcp` owns the page side (one tool registry for MCP and WebMCP, MCP 2026-07-28 only, no host calls); `core/lib/browser-mcp.ts` owns the agent-side transport and `mcp/lib/connect.js` its stdio adapter. Expose no binding or callable route to the page, treat everything it returns (descriptions, annotations, results, errors) as untrusted website text, bind a connection to one origin and provider instance, and never retry a call interrupted by a reload. The static board composition (`core/lib/browser-board.ts`) runs the shared contract tools on `BrowserRuntime`; keep its parity vectors with the Node writer green and report Node-only capabilities as absent, never emulated.
53
54
  - **Host capabilities are advertised, not assumed:** anything only one host can do (the Pi extension can change the session's model and thinking level; an MCP client cannot) is published as data in the board state (`agent.controls`), turned into a control when present and **absent** when not — never a disabled control, never a guessed value. The board stores no session setting as project content, asks only for values the host listed, and shows what the host reported applying. The host is the authority; the board is a view.
54
55
  - **Scope discipline:** Preserve unrelated work. Do not publish, tag or deploy without explicit authorization.
55
56
 
56
57
  ## Interface rules
57
58
 
58
- - **Chrome layout:** Islands stay in place: DIVLET button and search (top left, separate pills), the three tabs (bottom left, separate pills that join the sidebar when it opens) and the toolbar (bottom right) with the inspector attached above it. The sidebar (Layers / History of changes / Agent conversation) is one homogeneous surface: no horizontal border dividers, nested darker areas instead. Breadcrumbs sit bottom centre only (wrapping when narrow; sidebars cover them on phones). A single selected artboard folds the path to an accessible root-element icon; activating it selects the actual root and opens the path, while the artboard crumb folds it again. Keep keyboard focus in that path across folding/unfolding and refuse late root reads after a newer navigation. Select-root belongs here, not the artboard/inspector action menus; Rename is likewise absent from those menus, with direct name editing, R and palette access retained. A tab both switches and, when already open, folds the sidebar. Tab density follows the tab row's own width, not pane visibility: icons always stay to the left of complete text labels, never stacked above them. Counters may wrap within the text column when needed; folding must not reset that layout or change the tab row height. The composer's context header has intrinsic height and a real gap before the input, including when it wraps.
59
+ - **Chrome layout:** Islands stay in place: DIVLET button and search (top left, separate pills), the three tabs (bottom left, separate pills that join the sidebar when it opens) and the toolbar (bottom right) with the inspector attached above it. The sidebar (Layers / History of changes / Agent conversation) is one homogeneous surface: no horizontal border dividers, nested darker areas instead. Breadcrumbs sit bottom centre only (wrapping when narrow; sidebars cover them on phones). The artboard crumb is never hidden. With a single artboard selected it is the current crumb, followed by a separator and a `root` crumb — the one exception to ending at the selection, previewing the next level; activating it selects the actual root, and the artboard crumb selects the artboard again. Keep keyboard focus in the path across these changes and refuse late root reads after a newer navigation. Select-root belongs here, not the artboard/inspector action menus; Rename is likewise absent from those menus, with direct name editing, R and palette access retained. A tab both switches and, when already open, folds the sidebar. Tab density follows the tab row's own width, not pane visibility: icons always stay to the left of complete text labels, never stacked above them. Counters may wrap within the text column when needed; folding must not reset that layout or change the tab row height. The composer's context header has intrinsic height and a real gap before the input, including when it wraps.
59
60
  - **One filesystem browser:** `web/lib/file-picker.js` owns the reusable path field, parent navigation, listing rows, keyboard navigation and latest-read/disposal guards. Open/copy and Settings compose it instead of maintaining separate browsers. Inject the read-only listing; keep confirmation, relative-output conversion, project writes and modal lifetime in the owning workflow. Reuse it for equivalent filesystem choices, not for resource galleries with different admission semantics.
60
61
  - **Two homes, at most, for an action:** Keep one natural control location plus the general indexes (shortcuts, command palette/search, agent tools), never duplicated interface controls. Before adding one, name its homes and search for existing affordances; move or merge rather than copy. Closing the left sidebar is the active tab (or `L`/`H`), not a separate panel/corner close button.
61
62
  - **UX stability:** Errors, statuses and progress never move controls: messages render after the controls they concern, progress is drawn on the element that is working, action buttons keep a stable size. The Agent activity plaque is a five-column, three-row grid: indicator at the start, activity text across the upper rows, time in the footer and Stop in the bottom-right cell; do not squeeze them back into one flex row. While busy and the entire left sidebar is folded, move that same plaque above the tabs at the remembered sidebar width, never clone it or add a second Stop. In this floating form, put the spinner in the bottom-left cell, vertically centred with the timer and Stop in the footer, and let the description span the full width above; the open-panel indicator stays in its original upper cell. Tabs stay put; long floating activity scrolls inside its bounded text area, and phone sheets clear the measured footer. Idle and expanded browsing show no floating plaque.
package/BACKLOG.md CHANGED
@@ -45,8 +45,9 @@ Checkpoint: managed HTML edits map back to the correct native objects through gu
45
45
 
46
46
  The user authorized incremental implementation of the [browser runtime direction](docs/browser-runtime.md). Scope: shared portable semantics, a capability-truthful static demo and separately proven browser-agent connectivity; not unrelated product/assets work, a wholesale rewrite or additional format-freeze prerequisites. Continue through independently validated checkpoints. No publication, deployment or Service-Worker-as-public-MCP assumption.
47
47
 
48
- - [ ] **BROWSER-DEMO — Prove a static-hosted, capability-truthful board demonstration.** Next provide a semantic prepared-preview path and hide/refuse mutation affordances using actual host capabilities rather than synthesizing HTTP routes. Compose the existing allowlisted public-page producer and read-session/snapshot seam with browser-local selection/observations, absent native History/source-update services and the real board UI under a repository prefix, without Node API/SSE rescue. Audit project/codec/assets/tool-schema dependencies only as needed; standalone-frame and opaque snapshot proofs do not establish whole-project acceptance, component expansion or rendering/resource support. Keep onboarding readable and the demo opt-in; no second authored landing or recursive editor iframe. Verify clipboard actions, private-project non-upload, accurate absent capabilities, first visit versus offline reload, versioned cache updates and startup/bundle cost. Offline editing additionally needs persistence/failure/download/reopen and multi-tab ownership evidence; do not infer it from a read-only demo.
49
- - [ ] **BROWSER-AGENT — Validate browser-native tools and external-agent transport separately.** Inspect WebMCP support and demonstrate real tool registration/call/refusal/cancellation with a participating browser agent before adopting an adapter. The Community Group draft and a property probe are not interoperability proof. For ordinary external MCP clients, prototype only an explicitly paired local/extension bridge with origin/document/project/session capabilities and browser network-policy checks; no default cloud relay, wildcard CORS, localhost probing or Service-Worker-as-public-server claim. Preserve the existing operation vocabulary and authenticated preview boundary.
48
+ - [ ] **BROWSER-DEMO — Finish the static board beyond its first deployment.** The Pages build runs the real board on `BrowserRuntime` with absent native capabilities, IndexedDB persistence and Chromium evidence (`web/tests/pages.mjs`); see [browser-hosted MCP](docs/browser-mcp.md). Still open: deployment itself (maintainer-authorized push to `main`), offline reload with a versioned Service Worker cache, startup/bundle cost measurement (the host bundle carries parse5/PostCSS), multi-tab ownership of one browser copy, and an explicit reset-to-published control. Utility compilation, embedded assets and fonts in the browser need a browser style-engine composition and a portable WHATWG MIME/base64 slice with the Node decoder's exact canonicalization; until then they stay refused or reported, never approximated.
49
+ - [ ] **BROWSER-WRITER — Share one writer orchestration between Node and the browser.** `BrowserRuntime` mirrors `runtime.js` step for step over shared kernel modules and is held to it by `core/tests/browser-runtime.test.js`. Extract the accepted-state/history orchestration so both hosts run one implementation with persistence, publication and site hooks injected, then keep the parity vectors as regression evidence.
50
+ - [ ] **BROWSER-AGENT — Prove WebMCP with a participating browser agent; extend the browser MCP surface only with evidence.** The provider, transport and `--connect` adapter are implemented and tested against the official SDK; WebMCP registration is tested only against a draft-shaped stand-in because no available Chromium ships `document.modelContext`. Still open: a real WebMCP agent calling, refusing and cancelling a tool; a native Pi `/connect` (no stdio hop); page-driven `notifications/tools/list_changed` through `subscriptions/listen`; attaching to an already running user browser. Resources, prompts, multi-round-trip input and tasks are not planned until a provider needs them.
50
51
  - [ ] **BROWSER-NODE22-COLD — Capture a reliable reproduction of a pre-model package-reader refusal.** One default-parallel focused run on Node 22.19 raised `ERR_INVALID_PACKAGE_CONFIG` for the valid repository manifest during the new cold-source import, before model execution. An isolated import, serial 125-check run, three controlled parallel runs and the full 569-check parallel unit suite passed; that does not establish the cause or a fix. Retain `artifacts/browser-model-portability-first-failure.log` and diagnostic logs. If it recurs, capture manifest identity/read/descriptor diagnostics and compare the unchanged baseline; do not add retry-until-green, weaken the cold-provider assertion or claim proven flakiness. Further causal work is gated on reproduction in a frozen checkout rather than repeated unchanged reruns; concurrent manifest edits occurred in this shared checkout and must be excluded before attributing the failure to Node or the model.
51
52
 
52
53
  ## P1 — onboarding template initialization
@@ -83,6 +84,4 @@ Complete the remaining migration under `strict` + `erasableSyntaxOnly`, preservi
83
84
 
84
85
  ## Gated
85
86
 
86
- - [ ] **NPM-CI — Finish the first trusted CI publication.** Three pre-publication runs exposed two test preconditions (initial dock measurement and font-dependent paired-row width) and the project picker's fixed-width primary button. Those are corrected; the button regression now forces wider fonts and preserves the exact control-position assertion. All publisher jobs remained skipped. The maintainer authorized replacement of the failed `v0.0.2` tag only after refreshed no-consumed-release-state checks. Complete a correlated successful CI run, five registry-integrity/installed-host checks and GitHub Release. Preserve original failed-run evidence; npm bindings are not verified until actual OIDC publication. See [publishing](docs/publishing.md#github-actions-publishing).
87
- - [ ] **RELEASE-GIT — Complete GitHub release delivery only when separately authorized.** Verify origin/permissions, remote history, automation and the intended `v0.0.1` target against the published npm candidate (`ea62291`, manifest in ignored `artifacts/releases/0.0.1-flat/`). Preserve immutable published bytes and local history; do not force-rewrite main or move a consumed release tag. Pushing the prepared source/cache changes can trigger Pages, so that deployment consequence also needs explicit authorization. See [publication](docs/publishing.md).
88
87
  - [ ] **LOCAL-PI — Refresh a legacy checkout installation only with operator approval.** Use the built top-level `pi/` package, never the root or removed `packages/pi`/`adapters/pi` paths. Coordinate local dependency refresh and reload with the session owner; do not restart active owners or discard their History as an installation shortcut.
package/CHANGELOG.md CHANGED
@@ -2,11 +2,20 @@
2
2
 
3
3
  Implemented product outcomes live here. Contributor constraints are in [AGENTS.md](AGENTS.md), current contracts in [docs/](docs/README.md), and remaining work in [BACKLOG.md](BACKLOG.md).
4
4
 
5
- ## 0.0.2 — candidate
5
+ ## 0.0.3 — candidate
6
+
7
+ - **The board runs as a static site that is its own MCP provider.** `scripts/pages-build.mjs` builds the GitHub Pages set: the real board client with a bundled browser host that opens the canonical landing project in the visitor's browser, keeps the visitor's copy in IndexedDB until the publication changes, and exposes `read_divlet`, `patch_divlet`, `edit_divlet`, `frame_divlet`, `undo_divlet` and `mark_divlet` through the page's MCP provider (and WebMCP when a browser offers it). A first visit reads the landing expanded at the window's width, with live updates, one step from the board. Native-only affordances (opening files, save-as, build output, image/font upload, the board conversation) are absent there; Copy/Download HTML and "Download project file" run in the page. The Pages workflow tests the built set in Chromium and publishes it only when its fingerprint changes. Not yet deployed.
8
+ - **`divlet-mcp --connect <url>`** connects any MCP host to such a website: the site opens in a managed, isolated Chromium and its tools run in the page. Both MCP eras are answered on stdio; website descriptions are marked as website text, annotations are dropped, results are reduced to text and JSON, cross-origin redirects are refused and a reloaded page is reconnected explicitly.
9
+ - **Reusable browser MCP layers.** `@divlet/web/mcp` (one tool registry with bounded JSON Schema validation, an MCP 2026-07-28 browser provider with a versioned page endpoint, optional WebMCP registration) and `@divlet/core/browser-mcp` (the agent-side transport, also as an SDK-shaped `Transport`). The official MCP TypeScript SDK client interoperates with both.
10
+ - **A browser-local project writer.** `BrowserRuntime` runs the shared contract tools over the shared kernel; shared vectors require identical models, pages, refusals, history and serialized projects to the Node runtime. `project`, `codec`, `diff`, `ops`, frame preparation, acceptance rules and the tool list no longer reach Node built-ins; embedded asset decoding refuses explicitly in browser builds.
11
+ - Breadcrumbs keep the selected artboard visible as the current crumb, followed by › and a `root` step into its root element, instead of folding the path to a lone icon.
12
+ - The board reaches its host through writer, preview, export and component ports; a complete alternative composition can replace the native HTTP routes without fallback.
13
+
14
+ ## 0.0.2 — 2026-11-13
6
15
 
7
16
  - Fixed Landing CI to resolve workspace sources in a fresh checkout, without depending on a previously generated web build. Browser activity checks now take their initial tab baseline after the newly visible toolbar reports its measured height, and paired typography checks use content width instead of a host-specific font-width assumption. Exact geometry assertions remain unchanged.
8
17
  - Project-picker action buttons reserve intrinsic space for all state labels, keeping controls fixed through loading/errors across host fonts and narrow layouts.
9
- - Added a manually dispatched `Release` GitHub Actions workflow with npm Trusted Publishing/OIDC, frozen five-package archives, serial web/core/adapter publication and registry-install verification through CLI/MCP and an isolated real Pi host. Preparation and verification have no publish token; uncertain or prior publication attempts block automatic replay. The first live CI publication is pending verification; this candidate does not freeze the native project format.
18
+ - Added a manually dispatched `Release` GitHub Actions workflow with npm Trusted Publishing/OIDC, frozen five-package archives, serial web/core/adapter publication and registry-install verification through CLI/MCP and an isolated real Pi host. Preparation and verification have no publish token; uncertain or prior publication attempts block automatic replay. The first live CI release verified all five registry integrities and actual installed CLI/MCP/Pi behavior without a stored npm token. The native project format remains a pre-freeze draft.
10
19
 
11
20
  ## 0.0.1 — 2026-11-13
12
21
 
package/README.md CHANGED
@@ -6,7 +6,7 @@ A local layout board for coding agents. Work with real HTML in live artboards, r
6
6
 
7
7
  [Quick start](#quick-start) · [Connect an agent](#connect-an-agent) · [Board guide](docs/board.md) · [Documentation](docs/README.md)
8
8
 
9
- > **0.0.2 · release candidate.** CI publication is being verified; 0.0.1 remains the last confirmed npm release until that completes. The native project format remains a pre-freeze draft; keep backups because future versions may require explicit conversion.
9
+ > **0.0.3 · release candidate.** CI publication is being verified; 0.0.2 remains the last confirmed npm release until that completes. The native project format remains a pre-freeze draft; keep backups because future versions may require explicit conversion.
10
10
 
11
11
  ## Real pages, not pictures of pages
12
12
 
@@ -22,7 +22,7 @@ Requires **Node.js 22.19 or later**. Pi is optional.
22
22
  Open a standalone board:
23
23
 
24
24
  ```bash
25
- npx -y @divlet/cli@0.0.2 /absolute/path/to/workspace
25
+ npx -y @divlet/cli@0.0.3 /absolute/path/to/workspace
26
26
  ```
27
27
 
28
28
  Or run from a source checkout:
@@ -35,7 +35,7 @@ npm start -- /absolute/path/to/workspace
35
35
 
36
36
  Open the URL printed in the terminal. An empty workspace gets `project.divlet` with a starter artboard; an existing project opens directly, and a folder with several projects opens the launcher.
37
37
 
38
- `npm run demo` opens the real onboarding page, [examples/landing/landing.divlet](examples/landing/landing.divlet). Its tracked, derived cache supplies GitHub Pages; see [editing and rebuilding the landing](examples/README.md). `npm run demo:site` opens the Markdown handbook in `examples/handbook/`; `examples/studio/` holds a separate layout scenario. Pencil import remains supported, but no authored PEN demo is bundled.
38
+ `npm run demo` opens the real onboarding page, [examples/landing/landing.divlet](examples/landing/landing.divlet). On GitHub Pages the same project opens in the real board running entirely in your browser, which is also an MCP provider for agents ([browser-hosted MCP](docs/browser-mcp.md)); see [editing and rebuilding the landing](examples/README.md). `npm run demo:site` opens the Markdown handbook in `examples/handbook/`; `examples/studio/` holds a separate layout scenario. Pencil import remains supported, but no authored PEN demo is bundled.
39
39
 
40
40
  **First moves:** `V` selects, `P` lets the page behave normally, `L` opens Layers and `H` opens History. `Ctrl K` finds commands; `?` shows shortcuts. See the [board guide](docs/board.md) for direct editing, the inspector and touch controls.
41
41
 
@@ -46,7 +46,7 @@ Open the URL printed in the terminal. An empty workspace gets `project.divlet` w
46
46
  Install the Pi adapter:
47
47
 
48
48
  ```bash
49
- pi install npm:@divlet/pi@0.0.2
49
+ pi install npm:@divlet/pi@0.0.3
50
50
  ```
51
51
 
52
52
  For checkout development, load the source adapter for a one-off session instead (no build needed):
@@ -84,7 +84,7 @@ Drop `--global` to install into the current project only. Skip this step under P
84
84
  "mcpServers": {
85
85
  "divlet": {
86
86
  "command": "npx",
87
- "args": ["-y", "@divlet/mcp@0.0.2", "--cwd", "/absolute/path/to/workspace"]
87
+ "args": ["-y", "@divlet/mcp@0.0.3", "--cwd", "/absolute/path/to/workspace"]
88
88
  }
89
89
  }
90
90
  }
@@ -92,6 +92,16 @@ Drop `--global` to install into the current project only. Skip this step under P
92
92
 
93
93
  Call `open_divlet` and open its returned URL. No Pi installation is needed. Clients that support **sampling** can also answer requests typed in the board's Agent tab.
94
94
 
95
+ ### A board on a website
96
+
97
+ The [Pages board](https://llblab.github.io/divlet/) is a static site that is its own MCP provider: register it by URL and the tools run in a browser page, on that site's project, with no server deployed for it. `--connect` ships with the 0.0.3 candidate of `@divlet/mcp`; until its publication is verified, run it from a checkout (`node scripts/source.mjs mcp/index.js --connect <url>`).
98
+
99
+ ```bash
100
+ npx -y @divlet/mcp@0.0.3 --connect https://llblab.github.io/divlet/
101
+ ```
102
+
103
+ The page opens in a managed browser window (headless without a display). Edits persist in that browser profile until the publication changes; utility compilation, images, fonts and opening other projects stay with the local server. Website descriptions and results are treated as website text, and redirects to other origins are refused. See [browser-hosted MCP](docs/browser-mcp.md) for the protocol, security boundaries and how to make another static app a provider.
104
+
95
105
  [Agent integration guide](docs/agents.md) → Alternative Pi installation, MCP options, the workflow skill and JavaScript entry points.
96
106
 
97
107
  <details>
@@ -110,7 +120,7 @@ npx -y @divlet/mcp --cwd ./workspace
110
120
  npm install -g @divlet/cli @divlet/mcp
111
121
  ```
112
122
 
113
- For Pi, run `pi install npm:@divlet/pi@0.0.2`. Shared setups should pin `@divlet/cli@0.0.2`, `@divlet/mcp@0.0.2` and `npm:@divlet/pi@0.0.2`. The root package has no commands or Pi manifest; there is no wrapper layer.
123
+ For Pi, run `pi install npm:@divlet/pi@0.0.3`. Shared setups should pin `@divlet/cli@0.0.3`, `@divlet/mcp@0.0.3` and `npm:@divlet/pi@0.0.3`. The root package has no commands or Pi manifest; there is no wrapper layer.
114
124
 
115
125
  See [npm publication](docs/publishing.md) for version pinning, package identity and release gates.
116
126
 
@@ -0,0 +1,18 @@
1
+ export declare const SETTINGS_FIELDS: readonly ["name", "tokens", "css", "metadata", "outputDir", "assets"];
2
+ export declare const sameDocument: (a: any, b: any) => boolean;
3
+ export declare const sameSettings: (a: any, b: any) => boolean;
4
+ export declare const settingsPatch: (from: any, to: any) => Record<string, unknown>;
5
+ /**
6
+ * A caller that names the state it read gets a refusal instead of silently overwriting a change it
7
+ * never saw (AGENTS.md, "Validated commits"). Omitted means the caller accepted whatever is current.
8
+ */
9
+ export declare function assertExpectedVersion(expected: unknown, target: {
10
+ version: number;
11
+ }, what?: string): void;
12
+ /** The rendered page a gesture was made against; a dependent re-render advances it without a version bump. */
13
+ export declare function assertExpectedRender(expected: unknown, entry: {
14
+ render: number;
15
+ document: {
16
+ version: number;
17
+ };
18
+ }): void;
@@ -0,0 +1,29 @@
1
+ /*
2
+ * Owns: the host-independent acceptance rules every project writer applies — what counts as an unchanged frame or project,
3
+ * project-field diffs for history, and the expected version/render guards. Excludes: state, persistence and scheduling.
4
+ * The Node runtime and the browser runtime both import these, so a revision or no-op decision cannot differ between hosts.
5
+ */
6
+ import { DivletError } from "./model.js";
7
+ import { diffValue } from "./diff.js";
8
+ import { sameJson } from "./validation.js";
9
+ export const SETTINGS_FIELDS = ['name', 'tokens', 'css', 'metadata', 'outputDir', 'assets'];
10
+ export const sameDocument = (a, b) => sameJson({ ...a, version: 0 }, { ...b, version: 0 });
11
+ export const sameSettings = (a, b) => SETTINGS_FIELDS.every(key => sameJson(a[key], b[key]));
12
+ export const settingsPatch = (from, to) => Object.fromEntries(SETTINGS_FIELDS.map(key => [key, diffValue(from[key], to[key])]).filter(([, value]) => value !== undefined));
13
+ /**
14
+ * A caller that names the state it read gets a refusal instead of silently overwriting a change it
15
+ * never saw (AGENTS.md, "Validated commits"). Omitted means the caller accepted whatever is current.
16
+ */
17
+ export function assertExpectedVersion(expected, target, what = 'frame') {
18
+ if (expected === undefined)
19
+ return;
20
+ if (!Number.isInteger(expected) || expected !== target.version)
21
+ throw new DivletError('REVISION_CONFLICT', `Expected ${what} version ${target.version}; received ${expected}. Read it again and rebuild the change.`, { actualVersion: target.version });
22
+ }
23
+ /** The rendered page a gesture was made against; a dependent re-render advances it without a version bump. */
24
+ export function assertExpectedRender(expected, entry) {
25
+ if (expected === undefined)
26
+ return;
27
+ if (!Number.isInteger(expected) || expected !== entry.render)
28
+ throw new DivletError('RENDER_CONFLICT', `Expected frame render ${entry.render}; received ${expected}. The page changed since it was read; reload the frame and repeat the change.`, { actualRender: entry.render, actualVersion: entry.document.version });
29
+ }
@@ -104,7 +104,7 @@ function readTree(input, deletions) {
104
104
  if (paths.has(portable))
105
105
  fail(`Asset paths alias on a portable filesystem: ${relative}.`);
106
106
  paths.add(portable);
107
- encodedBytes += Buffer.byteLength(name) + (typeof value === 'string' ? value.length : 0);
107
+ encodedBytes += new TextEncoder().encode(name).byteLength + (typeof value === 'string' ? value.length : 0);
108
108
  if (encodedBytes > ASSET_LIMITS.encodedBytes)
109
109
  fail('Assets exceed the encoded-byte limit.');
110
110
  if (value === null && deletions)
@@ -0,0 +1,41 @@
1
+ import { BrowserRuntime } from './browser-runtime.ts';
2
+ import type { FrameCompiler } from './prepare.ts';
3
+ /** Build-time compiled CSS, keyed by the token set it was compiled for (see `scripts/pages-build.mjs`). */
4
+ export interface StyleSnapshot {
5
+ readonly entries: ReadonlyArray<{
6
+ readonly tokens: Record<string, string>;
7
+ readonly candidates: readonly string[];
8
+ readonly css: string;
9
+ }>;
10
+ }
11
+ export interface BrowserBoardOptions {
12
+ readonly projectUrl: URL;
13
+ readonly stylesUrl: URL;
14
+ readonly storageKey: string;
15
+ readonly file: string;
16
+ readonly version: string;
17
+ /** The static preview bridge script (`previewBridgeScript()` output), served by the same site. */
18
+ readonly bridgeUrl: URL;
19
+ /** The frame a first visit opens as a readable page (absent: the board fits all frames). */
20
+ readonly entryFrame?: string;
21
+ readonly target?: object;
22
+ }
23
+ /**
24
+ * The browser host compiles no utilities: it serves the build-time stylesheet for the frame's token set, and reports a class
25
+ * as unknown when neither that build nor the frame's own CSS mentions it, so new utilities are diagnosed instead of dropped.
26
+ */
27
+ export declare function snapshotCompiler(snapshot: StyleSnapshot): FrameCompiler;
28
+ /**
29
+ * The preview page of one frame: the rendered HTML, a meta CSP and the static preview bridge with its arguments in a meta tag.
30
+ * A blob preview inherits the board page's CSP, so the bridge is a same-site script file rather than an inline script.
31
+ */
32
+ export declare function previewDocument(html: string, id: string, render: number, bridge: string, bridgeUrl: string): string;
33
+ /** The static bridge file: the trusted serialized preview bridge, started with the arguments its page carries. */
34
+ export declare function previewBridgeScript(bridgeSource: string): string;
35
+ /** Start the browser host and publish the board composition; resolves once the board can start. */
36
+ export declare function startBrowserBoard(options: BrowserBoardOptions): Promise<{
37
+ runtime: BrowserRuntime;
38
+ restored: boolean;
39
+ webmcp: string[];
40
+ base: string;
41
+ }>;
@@ -0,0 +1,203 @@
1
+ /*
2
+ * Owns: the static-site composition of the real board — one browser-local copy of a published project, the board's host ports
3
+ * over the browser writer, and the same contract tools exposed to agents through the page's browser MCP provider (and WebMCP
4
+ * when the browser has it). Excludes: the board UI itself (`@divlet/web`), any network service and the Node host's file,
5
+ * output, asset, font, site and conversation capabilities, which are absent here rather than emulated.
6
+ *
7
+ * Storage policy: the visitor's copy lives in this origin's IndexedDB, keyed by `storageKey` and tied to the fingerprint of the
8
+ * published source it started from. When the publication changes, the copy starts again from the new source. Nothing is
9
+ * uploaded; a private project is never fetched or paired.
10
+ */
11
+ import { BrowserRuntime } from "./browser-runtime.js";
12
+ import { inputSchema, forAgent } from "./tools.js";
13
+ import { sameJson } from "./validation.js";
14
+ import { createBrowserMcpProvider, exposeBrowserMcp, defineTools, registerWebMcpTools } from '@divlet/web/mcp';
15
+ const HOST_KEY = Symbol.for('divlet.board-host/1');
16
+ const READ_ONLY = new Set(['read_divlet']);
17
+ /**
18
+ * The browser host compiles no utilities: it serves the build-time stylesheet for the frame's token set, and reports a class
19
+ * as unknown when neither that build nor the frame's own CSS mentions it, so new utilities are diagnosed instead of dropped.
20
+ */
21
+ export function snapshotCompiler(snapshot) {
22
+ const pick = (tokens) => snapshot.entries.find(entry => sameJson(entry.tokens, tokens)) ?? snapshot.entries[0];
23
+ return {
24
+ compile: tokens => pick(tokens)?.css ?? '',
25
+ unknown: ({ tokens, css, byNode }) => {
26
+ const known = new Set(pick(tokens)?.candidates ?? []);
27
+ const out = {};
28
+ for (const [id, className] of Object.entries(byNode)) {
29
+ const dead = className.split(/\s+/).filter(name => name && !known.has(name) && !css.includes(`.${name}`));
30
+ if (dead.length)
31
+ out[id] = dead;
32
+ }
33
+ return out;
34
+ },
35
+ };
36
+ }
37
+ async function fingerprint(text) {
38
+ const bytes = new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text)));
39
+ return [...bytes].map(byte => byte.toString(16).padStart(2, '0')).join('');
40
+ }
41
+ function browserStore(name = 'divlet-browser') {
42
+ const open = () => new Promise((resolve, reject) => {
43
+ const request = globalThis.indexedDB.open(name, 1);
44
+ request.onupgradeneeded = () => request.result.createObjectStore('projects');
45
+ request.onsuccess = () => resolve(request.result);
46
+ request.onerror = () => reject(request.error);
47
+ });
48
+ let db = null;
49
+ const handle = () => (db ??= open());
50
+ return {
51
+ async get(key) {
52
+ try {
53
+ const store = (await handle()).transaction('projects').objectStore('projects');
54
+ return await new Promise((resolve, reject) => { const request = store.get(key); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); });
55
+ }
56
+ catch {
57
+ return undefined;
58
+ }
59
+ },
60
+ async put(key, value) {
61
+ const tx = (await handle()).transaction('projects', 'readwrite');
62
+ tx.objectStore('projects').put(value, key);
63
+ await new Promise((resolve, reject) => { tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); tx.onabort = () => reject(tx.error); });
64
+ },
65
+ async delete(key) {
66
+ const tx = (await handle()).transaction('projects', 'readwrite');
67
+ tx.objectStore('projects').delete(key);
68
+ await new Promise((resolve, reject) => { tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); });
69
+ },
70
+ };
71
+ }
72
+ const escapeAttribute = (value) => value.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;');
73
+ /**
74
+ * The preview page of one frame: the rendered HTML, a meta CSP and the static preview bridge with its arguments in a meta tag.
75
+ * A blob preview inherits the board page's CSP, so the bridge is a same-site script file rather than an inline script.
76
+ */
77
+ export function previewDocument(html, id, render, bridge, bridgeUrl) {
78
+ const token = /^[a-f0-9-]{36}$/i.test(bridge) ? bridge : '';
79
+ // A path may legally contain CSP separators; encode them so the bridge address cannot add or end a directive.
80
+ const source = bridgeUrl.replace(/[;,\s'"]/g, character => `%${character.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0')}`);
81
+ const csp = `default-src 'none'; img-src data: https: blob:; media-src data: https:; style-src 'unsafe-inline'; font-src data: https:; script-src ${source}; connect-src 'none'; base-uri 'none'; form-action 'none'`;
82
+ const meta = `<meta http-equiv="Content-Security-Policy" content="${escapeAttribute(csp)}"><meta name="divlet-bridge" content="${escapeAttribute(JSON.stringify({ id, render, token }))}">`;
83
+ return html.replace(/<head([^>]*)>/i, `<head$1>${meta}`).replace(/<\/body>(\s*<\/html>\s*)?$/i, `<script src="${escapeAttribute(source)}"></script></body>$1`);
84
+ }
85
+ /** The static bridge file: the trusted serialized preview bridge, started with the arguments its page carries. */
86
+ export function previewBridgeScript(bridgeSource) {
87
+ return `(()=>{const m=document.querySelector('meta[name="divlet-bridge"]');if(!m)return;const a=JSON.parse(m.content);(${bridgeSource})(a.id,a.render,a.token,false,false);})();\n`;
88
+ }
89
+ /** Start the browser host and publish the board composition; resolves once the board can start. */
90
+ export async function startBrowserBoard(options) {
91
+ const target = options.target ?? globalThis;
92
+ const [published, styles] = await Promise.all([
93
+ fetch(options.projectUrl, { credentials: 'omit' }).then(response => { if (!response.ok)
94
+ throw new Error(`The project could not be loaded (${response.status}).`); return response.text(); }),
95
+ fetch(options.stylesUrl, { credentials: 'omit' }).then(response => { if (!response.ok)
96
+ throw new Error(`The project styles could not be loaded (${response.status}).`); return response.json(); }),
97
+ ]);
98
+ const base = await fingerprint(published);
99
+ const store = browserStore();
100
+ const stored = await store.get(options.storageKey);
101
+ const compiler = snapshotCompiler(styles);
102
+ const persist = (text) => store.put(options.storageKey, { base, text, savedAt: new Date().toISOString() });
103
+ const open = (source) => BrowserRuntime.open({ source, file: options.file, projectId: `browser-${base.slice(0, 16)}`, compiler, persist, capabilities: { browser: true, assets: false, export: false } });
104
+ let runtime;
105
+ let restored = false;
106
+ if (stored?.base === base && typeof stored.text === 'string') {
107
+ try {
108
+ runtime = await open(stored.text);
109
+ restored = true;
110
+ }
111
+ catch {
112
+ await store.delete(options.storageKey).catch(() => { });
113
+ runtime = await open(published);
114
+ }
115
+ }
116
+ else {
117
+ if (stored)
118
+ await store.delete(options.storageKey).catch(() => { });
119
+ runtime = await open(published);
120
+ }
121
+ runtime.url = new URL('./', globalThis.location.href).href;
122
+ /* ---- the board's ports ---------------------------------------------------------------------------- */
123
+ const copy = (value) => structuredClone(value);
124
+ const previews = new Map();
125
+ const composition = Object.freeze({
126
+ label: 'browser',
127
+ readSession: Object.freeze({
128
+ readState: async () => copy(runtime.info()),
129
+ readFrame: async (id) => copy(runtime.read({ view: 'document', documentId: id })),
130
+ subscribe(listener) {
131
+ let active = true;
132
+ queueMicrotask(() => { if (active)
133
+ listener({ type: 'connection', status: 'connected' }); });
134
+ const stop = runtime.subscribe((type, value) => { if (active)
135
+ listener({ type, value: copy(value) }); });
136
+ return () => { active = false; stop(); };
137
+ },
138
+ }),
139
+ writer: Object.freeze({
140
+ patch: async (body) => copy(await runtime.patch({ ...body, actor: 'user' })),
141
+ edit: async (body) => copy(await runtime.edit({ ...body, actor: 'user' })),
142
+ undo: async () => copy(await runtime.undo({ actor: 'user' })),
143
+ redo: async () => copy(await runtime.redo({ actor: 'user' })),
144
+ createArtboard: async (body) => copy(await runtime.createArtboard({ ...body, actor: 'user' })),
145
+ deleteArtboard: async (body) => copy(await runtime.removeDocument({ ...body, actor: 'user' })),
146
+ }),
147
+ previews: Object.freeze({
148
+ frameUrl(id, { bridge }) {
149
+ const rendered = runtime.render(id);
150
+ const url = URL.createObjectURL(new Blob([previewDocument(rendered.html, id, rendered.render, bridge, options.bridgeUrl.href)], { type: 'text/html' }));
151
+ if (!previews.has(id))
152
+ previews.set(id, new Set());
153
+ previews.get(id).add(url);
154
+ return url;
155
+ },
156
+ readRender: async (id) => copy(runtime.render(id)),
157
+ release(id, url) { if (previews.get(id)?.delete(url))
158
+ URL.revokeObjectURL(url); },
159
+ }),
160
+ components: Object.freeze({ list: async () => ({ components: copy(runtime.components()) }) }),
161
+ exports: Object.freeze({
162
+ page: async (id) => copy(runtime.exportDocument(id)),
163
+ project: async () => ({ text: runtime.source(), fileName: options.file }),
164
+ }),
165
+ historyService: Object.freeze({
166
+ readRecent: async () => copy(runtime.readHistory({ limit: 100 })),
167
+ navigate: async (target) => copy(await runtime.navigateHistory({ ...target, actor: 'user' })),
168
+ }),
169
+ // Selection and measurements are shared with the browser writer, so an agent reads what the visitor selected and sees.
170
+ shareSelection: async (selection) => copy(runtime.setSelection(copy(selection))),
171
+ publishObservations: async (report) => copy(runtime.setObservations(copy(report))),
172
+ ...(options.entryFrame && runtime.documents.has(options.entryFrame) ? { initialView: Object.freeze({ expand: options.entryFrame, title: 'Edit on the board' }) } : {}),
173
+ });
174
+ Object.defineProperty(target, HOST_KEY, { value: composition, configurable: false, enumerable: false, writable: false });
175
+ /* ---- the agent surface: the same contract tools ------------------------------------------------- */
176
+ let running = 0;
177
+ const tools = runtime.tools().map(tool => ({
178
+ name: tool.name,
179
+ ...(tool.title ? { title: tool.title } : {}),
180
+ description: tool.description,
181
+ inputSchema: inputSchema(tool),
182
+ annotations: READ_ONLY.has(tool.name) ? { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } : { readOnlyHint: false, destructiveHint: tool.name !== 'mark_divlet', openWorldHint: false },
183
+ async execute(args) {
184
+ running++;
185
+ runtime.setAgent({ connected: true, busy: true, activity: tool.title ?? tool.name, queued: running - 1 });
186
+ try {
187
+ return forAgent(await runtime.callTool(tool.name, args));
188
+ }
189
+ finally {
190
+ running--;
191
+ runtime.setAgent(running ? { busy: true, queued: running - 1 } : { busy: false });
192
+ }
193
+ },
194
+ }));
195
+ const registry = defineTools(tools);
196
+ const provider = createBrowserMcpProvider({
197
+ name: 'divlet', title: 'divlet in the browser', version: options.version, tools: registry,
198
+ instructions: `divlet is a layout board for agents. This page holds a browser-local copy of the ${options.file} project (HTML/SVG/CSS frames), stored only in this browser. Read the project with read_divlet, then change it with patch_divlet or edit_divlet using the versions you read; frame_divlet adds or removes frames and undo_divlet reverts. The visitor watches every change live. Utility classes are not compiled here: style with the frame css or inline style. Files, fonts, embedded assets and opening other projects need the local divlet host (npx -y @divlet/mcp).`,
199
+ });
200
+ exposeBrowserMcp(provider, target);
201
+ const webmcp = await registerWebMcpTools(registry);
202
+ return { runtime, restored, webmcp: webmcp.registered, base };
203
+ }
@@ -0,0 +1,131 @@
1
+ export declare const PROTOCOL_VERSION = "2026-07-28";
2
+ /** The page surface this module drives (a Playwright `Page`, typed locally so core needs no Playwright types). */
3
+ interface PageLike {
4
+ goto(url: string, options?: {
5
+ waitUntil?: 'load' | 'domcontentloaded';
6
+ timeout?: number;
7
+ }): Promise<unknown>;
8
+ url(): string;
9
+ evaluate<R, A>(fn: (arg: A) => R | Promise<R>, arg: A): Promise<R>;
10
+ waitForFunction<A>(fn: (arg: A) => unknown, arg: A, options?: {
11
+ timeout?: number;
12
+ polling?: 'raf' | number;
13
+ }): Promise<unknown>;
14
+ waitForLoadState(state?: 'load' | 'domcontentloaded', options?: {
15
+ timeout?: number;
16
+ }): Promise<void>;
17
+ mainFrame(): unknown;
18
+ on(event: string, listener: (...args: any[]) => void): unknown;
19
+ off(event: string, listener: (...args: any[]) => void): unknown;
20
+ close(): Promise<void>;
21
+ isClosed(): boolean;
22
+ bringToFront?(): Promise<void>;
23
+ }
24
+ interface ContextLike {
25
+ newPage(): Promise<PageLike>;
26
+ close(): Promise<void>;
27
+ on(event: string, listener: (...args: any[]) => void): unknown;
28
+ }
29
+ interface BrowserLike {
30
+ newContext(options?: Record<string, unknown>): Promise<ContextLike>;
31
+ close(): Promise<void>;
32
+ on(event: string, listener: (...args: any[]) => void): unknown;
33
+ }
34
+ export type ConnectionState = 'connecting' | 'ready' | 'stale' | 'closed';
35
+ export interface ToolInfo {
36
+ readonly name: string;
37
+ readonly title?: string;
38
+ readonly description: string;
39
+ readonly inputSchema: Record<string, unknown>;
40
+ readonly annotations?: Record<string, unknown>;
41
+ }
42
+ export interface BrowserMcpOptions {
43
+ /** Origins besides the URL's own that the page may redirect to. Default: none — a redirect elsewhere fails. */
44
+ readonly allowedOrigins?: readonly string[];
45
+ /** Which website-supplied tools the agent may call. Default: all listed tools (the URL itself is the user's explicit choice). */
46
+ readonly allowTools?: readonly string[] | ((tool: ToolInfo) => boolean);
47
+ readonly timeoutMs?: number;
48
+ readonly readyTimeoutMs?: number;
49
+ readonly maxMessageBytes?: number;
50
+ readonly maxPending?: number;
51
+ readonly maxTools?: number;
52
+ readonly headless?: boolean;
53
+ /** Reuse a caller-owned browser (a new isolated context is still created) or a caller-owned context. */
54
+ readonly browser?: BrowserLike;
55
+ readonly context?: ContextLike;
56
+ /** A Chromium executable or Playwright channel to launch; by default the system Chrome, then Playwright's Chromium. */
57
+ readonly executablePath?: string;
58
+ readonly channel?: string;
59
+ /** Opt-in persistent profile directory (so the site's IndexedDB survives restarts). Default: an ephemeral context. */
60
+ readonly userDataDir?: string;
61
+ readonly clientInfo?: {
62
+ name: string;
63
+ version: string;
64
+ };
65
+ readonly onEvent?: (event: BrowserMcpEvent) => void;
66
+ }
67
+ export type BrowserMcpEvent = {
68
+ type: 'ready';
69
+ origin: string;
70
+ instance: string;
71
+ tools: number;
72
+ } | {
73
+ type: 'stale';
74
+ reason: string;
75
+ } | {
76
+ type: 'closed';
77
+ reason: string;
78
+ } | {
79
+ type: 'warning';
80
+ message: string;
81
+ };
82
+ /** A local failure of this side (never a JSON-RPC error from the page). */
83
+ export declare class BrowserMcpError extends Error {
84
+ readonly code: string;
85
+ readonly data?: unknown;
86
+ constructor(code: string, message: string, data?: unknown);
87
+ }
88
+ /** A JSON-RPC error the page returned. */
89
+ export declare class BrowserMcpRemoteError extends Error {
90
+ readonly code: number;
91
+ readonly data?: unknown;
92
+ constructor(code: number, message: string, data?: unknown);
93
+ }
94
+ export interface CallOptions {
95
+ readonly signal?: AbortSignal;
96
+ readonly timeoutMs?: number;
97
+ }
98
+ export interface BrowserMcpConnection {
99
+ readonly url: string;
100
+ readonly origin: string;
101
+ readonly state: ConnectionState;
102
+ /** The provider's `server/discover` result for the current instance (untrusted, page-supplied). */
103
+ readonly discovery: Record<string, unknown> | null;
104
+ /** Tools of the current provider instance that the trust policy allows. Descriptions are untrusted, page-supplied text. */
105
+ readonly tools: readonly ToolInfo[];
106
+ /** Send one MCP request; per-request `_meta` is attached. Resolves with the result object. */
107
+ request(method: string, params?: Record<string, unknown>, options?: CallOptions): Promise<Record<string, unknown>>;
108
+ /** `tools/call` for an allowed tool. Resolves with the MCP `CallToolResult`. */
109
+ callTool(name: string, args?: Record<string, unknown>, options?: CallOptions): Promise<Record<string, unknown>>;
110
+ /** Re-discover after a reload (or navigate back to the URL when the page is gone from it); refreshes the tool list. */
111
+ reconnect(): Promise<void>;
112
+ close(): Promise<void>;
113
+ /** The managed page, for an embedding harness that shows or screenshots it. */
114
+ readonly page: PageLike;
115
+ }
116
+ /** Open `url` and connect to its browser MCP provider. */
117
+ export declare function connectBrowserMcp(url: string, options?: BrowserMcpOptions): Promise<BrowserMcpConnection>;
118
+ /**
119
+ * An SDK-shaped `Transport` (start/send/close/onmessage/onclose/onerror) over one connection, for an MCP client library that
120
+ * accepts custom transports. It carries messages as they are; the client attaches its own per-request `_meta`. When the
121
+ * provider goes stale or closes, the transport closes, so the client rejects its pending requests and must connect again.
122
+ */
123
+ export declare function browserMcpTransport(connection: BrowserMcpConnection): {
124
+ onmessage: ((message: unknown) => void) | undefined;
125
+ onclose: (() => void) | undefined;
126
+ onerror: ((error: Error) => void) | undefined;
127
+ start(): Promise<void>;
128
+ send(message: unknown): Promise<void>;
129
+ close(): Promise<void>;
130
+ };
131
+ export {};