npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@aidenlab/juicebox-remote

v2.1.0

Published

Attach a juicebox.js viewer to a juicebox-mcp room so an LLM can drive it and peers stay in sync

Readme

@aidenlab/juicebox-remote

Attach a juicebox.js viewer to a room on the juicebox-mcp server, so an LLM connected over MCP can drive it and every page in the room stays in sync.

npm install juicebox.js @aidenlab/juicebox-remote

juicebox.js is a peer dependency (>=4.12.0 <5). The package never imports it: you pass in your own namespace import, so the viewer is bundled once. Plain ESM, no build step.

Embed example

<div id="viewer"></div>
<script type="module">
  import * as hic from 'juicebox.js'
  import { attachRemote } from '@aidenlab/juicebox-remote'

  const container = document.getElementById('viewer')
  await hic.init(container, {})

  const remote = attachRemote({
    hic,
    container,
    url: 'wss://jbmcp.3dg.io/ws',
    room: new URLSearchParams(location.search).get('room') ?? undefined,
    onStatus: (status) => { document.title = `juicebox (${status})` },
    onToolCall: (name) => console.info(`AI ran ${name}`),
  })

  // Share remote.joinUrl once the status is 'open'.
</script>

The server accepts a WebSocket only from an Origin on its allow-list, so a new host page needs its origin added to the server's ALLOWED_ORIGINS.

API

attachRemote(options)

| Option | | | |---|---|---| | hic | required | the juicebox.js namespace import | | container | required | the element passed to hic.init; used by hic.restoreSession | | url | required | the server's WebSocket endpoint, e.g. 'wss://jbmcp.3dg.io/ws' | | room | optional | the room to join. Omit it and the server mints one, so a page can start a room with no MCP client involved | | onStatus | optional | (status) => void, see below | | onToolCall | optional | (name) => void, called with a tool's name when the room reports that an MCP client ran it, e.g. for a toast | | createSocket | optional | (url) => WebSocket; defaults to the platform WebSocket |

It returns:

  • room — the room id, known once the status is 'open'; kept across a dropped connection.
  • joinUrl — the current page's URL with ?room=<id> set: open it to join the same room. undefined until the room is joined, and outside a browser.
  • detach() — unsubscribe from the viewer, close the socket and report 'closed'. Safe to call twice.

Statuses

| Status | Meaning | |---|---| | 'connecting' | a connection attempt started (also before every reconnect) | | 'open' | the room answered; room and joinUrl are set | | 'closed' | the connection dropped (it reconnects on its own, backing off to one attempt every 5 s) or detach() was called | | 'expired' | the room was idle for 24 h and is gone. The remote stops for good; start a new one with attachRemote and no room |

Joining a room

Once joined, the room's state wins: if another page is in the room, or the room holds a saved session, this page is replaced by it. A page that restored a snapshot link (?session=) before attaching therefore only seeds an empty room. After that the page saves its session to the room (checked every 10 s, sent only when it changed), so a later joiner catches up even when no other page is open.

Panels

A command may carry panel: a 1-based position among hic.getAllBrowsers() (left to right), a map name that only one panel shows, or "all". Omitted, it means the only panel when one is open and fails, listing the panels, when several are. A panel command's ack names the panel it acted on in result, one line per panel for "all". loadMap {panel: "new"} opens a browser with hic.createBrowser(container, …); closePanel removes one (never the last) with hic.deleteBrowser(browser), or browser.registry.delete(browser) on a juicebox.js without that export (4.7.0); getPanelList answers the list of panels and whether the Juicebox Panel Controls are shown.

Sync events are kept per panel too: every panel is followed, each event carries panel, the sender's position, and a peer applies it to its panel at that position, dropping it when it has none. Opening and closing a panel are sync events too: panelOpen {panel} opens an empty panel on a peer whose last panel is one before it (as loadMap {panel: "new"} does) and is dropped otherwise; panelClose {panel} closes the peer's panel at that position, never its last one. So positions stay aligned on every page, empty panels included, and a mapLoad only ever applies to a panel the peer has. An event without panel (from an older remote) applies to the current panel. A peer's getSession answer, which a late joiner restores, lists one entry per panel, {} for an empty one; the room's stored session (hic.compressedSession()) still skips them. ADR-0008.

Panel controls

The Juicebox Panel Controls (juicebox.js's navbar) are one switch for the whole page, on juicebox.js 4.12.0's browser registry. setPanelControlsVisible {show} sets it with hic.setNavbarVisible(show). A change on the page (the host's own button) is sent as the page-wide sync event panelControlsChange {visible}, which names no panel, and a peer applies it to its whole page. The state is in the session (navbarVisible), so a late joiner restoring the room's session matches. On a juicebox.js without setNavbarVisible the command fails and the event is dropped. ADR-0009.

Host requirements: the remote hears a panel open and close from juicebox.js's global BrowserAdd and BrowserDelete events (juicebox.js 4.12.0; not posted by a restore or a reset). Without them panels still open and close locally, but peers are not told. A host that creates a panel must also select it (hic.setCurrentBrowser), as juicebox-web's clone button and juicebox.js's own restore do: the remote scans the panels again on every selection (and after each command or catch-up it runs), which is how a restored session's panels, or any panel on a juicebox.js without BrowserAdd, get followed.

Protocol

@aidenlab/juicebox-remote/protocol exports the wire protocol's message catalogue and validators. The server imports it too; hosts do not need it.

No UI

The package renders nothing. A host that wants a connection indicator, a join-link QR code or a toast for tool calls builds it from onStatus, remote.joinUrl and onToolCall.

License

MIT