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

glovebox-client

v0.5.0

Published

Client SDK for talking to a deployed Glovebox server

Readme

glovebox-client

Client SDK for talking to a deployed Glovebox server. One WebSocket per session, multiple prompts multiplexed. Streams subscriber events and display slot pushes; resolves with the final assistant message and an outputs map of FileRefs the client can read back through the configured storage.

Install

pnpm add glovebox-client

Works in Node (uses ws) and the browser (uses the global WebSocket). The wire format and auth are identical.

Usage

Connecting

import { GloveboxClient } from "glovebox-client"

const client = GloveboxClient.make({
  endpoints: {
    media: { url: "wss://media.example.com/", key: process.env.GLOVEBOX_MEDIA_KEY! },
    docs:  { url: "wss://docs.example.com/",  key: process.env.GLOVEBOX_DOCS_KEY!  },
  },
})

const media = client.box("media")

client.box(name) lazily opens a connection on the first prompt and caches the Box afterward. The bearer key from the endpoint config is sent as Authorization: Bearer ... on the WS upgrade and on every subsequent HTTP request the SDK makes (/environment, /files/:id).

Prompting

import { readFile } from "node:fs/promises"

const result = media.prompt("Trim the first 30 seconds off in.mp4 and write trimmed.mp4.", {
  files: {
    "in.mp4": { mime: "video/mp4", bytes: await readFile("./in.mp4") },
  },
})

for await (const ev of result.events) {
  if (ev.event_type === "text_delta") process.stdout.write((ev.data as { text: string }).text)
}

const message = await result.message
const outputs = await result.outputs
const trimmed = await result.read("trimmed.mp4")
await writeFile("./trimmed.mp4", trimmed)

prompt(text, opts) returns a PromptResult synchronously — the call doesn't await the round-trip. The four async iterables / promises on the result fan out as the server sends frames:

| Member | Type | Settles when | |--------|------|--------------| | events | AsyncIterable<SubscriberEvent> | Closed at complete / error. | | display | AsyncIterable<DisplayEvent> | Closed at complete / error. Display events are session-scoped on the server, so they fan out to every active prompt. | | message | Promise<string> | Final assistant text from the complete frame. | | outputs | Promise<Record<string, FileRef>> | Outputs map from the complete frame. | | read(name) | Promise<Uint8Array> | Awaits outputs, looks up the named ref, fetches it through the configured ClientStorage. | | resolve(slot_id, value) | void | Sends a display resolution back to the server. | | reject(slot_id, error) | void | Sends a display rejection back. | | abort() | void | Sends { type: "abort", id }. |

Display slots

When a tool inside the agent calls display.pushAndWait(...), the server emits a display_push. Route slot pushes by slot.renderer, render the input, and call result.resolve(slot.id, value) once the user submits.

const result = media.prompt("Pick a frame to use as the thumbnail.")

for await (const ev of result.display) {
  if (ev.type === "push" && ev.slot) {
    const slot = ev.slot
    if (slot.renderer === "frame_picker") {
      const choice = await renderFramePicker(slot.input)
      result.resolve(slot.id, choice)
    }
  } else if (ev.type === "clear") {
    clearSlot(ev.slot_id!)
  }
}

Reading the environment

const env = await media.environment()
// env.fs.input.path === "/input"
// env.packages.apt?.includes("ffmpeg")

Cached after the first call. Useful when the same client holds many endpoints and routes prompts based on declared capabilities. Backed by GET /environment on the server.

Send-side errors

Outgoing frames (prompt, abort, display_resolve, display_reject) are dispatched through void this.send(...). Failures there are surfaced via:

const off = media.onSendError((err) => {
  console.error("[media] send failed:", err)
})
// later
off()

If no listener is registered, the SDK warns to console.warn so the failure doesn't disappear silently.

Lifecycle

await media.close()           // closes one box
await client.close()          // closes every cached box

Closing rejects every in-flight prompt's message and outputs and closes the corresponding event/display iterators. The connection is otherwise managed lazily — prompt() re-opens after close() only if you reach back through client.box(...) for a fresh Box.

Custom storage

DefaultClientStorage puts files inline (base64) and reads inline | url | server refs. That's enough for a lot of agents — tens of MB ride fine — but exceeds what you want over a single WS frame at some point.

Provide a ClientStorage to split big inputs out. The contract is two methods:

interface ClientStorage {
  put(name: string, mime: string, bytes: Uint8Array): Promise<FileRef>
  get(ref: FileRef, opts?: { bearer?: string }): Promise<Uint8Array>
}

Example: pre-sign an S3 URL on your backend, hand the s3 ref to the server.

import type { ClientStorage } from "glovebox-client"
import type { FileRef } from "glovebox-client"

class S3UploadingStorage implements ClientStorage {
  async put(name, mime, bytes): Promise<FileRef> {
    const { bucket, key, putUrl } = await fetch("/api/sign-upload", {
      method: "POST",
      body: JSON.stringify({ name, mime, size: bytes.length }),
    }).then((r) => r.json())
    await fetch(putUrl, { method: "PUT", body: bytes, headers: { "Content-Type": mime } })
    return { kind: "s3", name, mime, bucket, key }
  }

  async get(ref: FileRef, opts) {
    if (ref.kind === "s3") {
      const { getUrl } = await fetch("/api/sign-download", {
        method: "POST",
        body: JSON.stringify({ bucket: ref.bucket, key: ref.key }),
      }).then((r) => r.json())
      return new Uint8Array(await fetch(getUrl).then((r) => r.arrayBuffer()))
    }
    return new DefaultClientStorage().get(ref, opts)
  }
}

const client = GloveboxClient.make({
  endpoints: { media: { url, key } },
  storage: new S3UploadingStorage(),
})

The same storage is used for outputs: result.read(name) calls storage.get(ref, { bearer }), so s3 refs returned by the server flow through your custom adapter too. The bearer is forwarded only for server-kind refs (the kit's authenticated /files/:id route).

PromptOptions.inputs lets you pass pre-built FileRefs alongside (or instead of) raw files — handy when the bytes already live somewhere the server can read directly.

Errors

Server error frames reject result.message and result.outputs with an Error carrying the server-supplied code (assignable as (err as Error & { code: string }).code). The connection itself is preserved — only the failing prompt is dropped. A WS close drops every in-flight prompt with Error("Connection closed"). There is no automatic reconnect in v1; a fresh prompt on a closed Box throws.

Public surface

import {
  GloveboxClient,
  Box,
  DefaultClientStorage,
  type GloveboxClientOptions,
  type BoxEndpoint,
  type BoxOptions,
  type PromptOptions,
  type PromptResult,
  type SubscriberEvent,
  type DisplayEvent,
  type BoxEnvironment,
  type ClientStorage,
  type DefaultClientStorageOptions,
  type FileRef,
  type SubscriberEventType,
  type WireSlot,
} from "glovebox-client"

Status

v1. The wire protocol is protocol_version: 1. Connections are bearer-authed and prompts within a session run sequentially on the server. There is no client-side reconnect / resume — when the socket drops, in-flight prompts reject. JWT auth, multiplexed execution, and a hosted glovebox.dev tier are deferred to v2.

Companion packages

Documentation

License

MIT