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

@runmu.sh/ext-widgets

v1.0.0

Published

Lets the game show cards, menus, forms, tables and gauges in your client over GMCP Client.Widget, and tells it what you picked. Plain text only, with limits.

Readme

Server widgets (@runmu.sh/ext-widgets, id widgets)

Lets a game put small widgets in your client: a card (text and buttons), a menu (a list of choices), a form (fields and a submit), a table and a gauge. When you use one, the client sends its command and tells the game what you picked. Install it from Extensions → Discover and enable it for the worlds you trust: a game can show nothing until you do.

What a game sends is cut down before it is shown: plain text only (no HTML, no links, no images, no colours but the theme's), every text and list has a limit, at most 12 widgets per session, and a widget's command can only go to the game, never to the client's own Lua console.

Widgets appear on the session's widget layer: gauges stacked at the top left (220px), the rest stacked at the bottom right above the command bar. A card, menu or form that can be closed has a [ CLOSE ], and closes once you use it. New cards, menus and forms are read out by screen readers ("Form: Register. …").

GMCP contract

Package Client.Widget 1, announced to the game with Core.Supports.Add while the extension runs in the world. Each message has a schema in schema/ that μClient checks before the extension sees it (inbound) or before it is sent (outbound, in development).

| Message | Direction | Payload | |---|---|---| | Client.Widget.Show | game → client | a widget (below), or a one-item list holding it. Shows it, or replaces the one with the same id | | Client.Widget.Remove | game → client | { "id": … } or a bare id: remove that widget. {} or nothing: remove every widget of the session | | Client.Widget.Action | client → game | what the player used (below) |

Client.Widget.Show

| Field | Types | For | What | |---|---|---|---| | id | string, number | all | required; the game's name for it (64 characters) | | type | string | all | required: card, menu, form, table or gauge | | title | string | all | the heading (80) | | body | string | card, menu, form, table | plain text (2000); tags are removed, <br> and block ends become line breaks, entities are decoded | | buttons | [{ label, cmd? }] or labels | card | up to 8; label 40, cmd 500 | | options | [{ label, cmd? }] or labels | menu | up to 20 | | fields | [{ name, label?, type?, options?, value?, placeholder? }] | form | up to 8. name: A-Z a-z 0-9 _ . -, 32. type: input (default), select (up to 30 options), textarea | | cmd | string | form | the command template: {name} is replaced by that field's value (on one line) | | submit (or submit_label) | string | form | the submit label; default "Submit" | | columns | list | table | up to 8 headings (80 each) | | rows | list of lists | table | up to 40 rows; cells (80 each) beyond the columns are dropped | | value, max | number | gauge | max defaults to 100 | | label | string | gauge | the gauge's label (defaults to title) | | color | string | gauge | a theme colour: accent (default), accent-bright, ok, gold, alert | | order | number | gauge | its place among gauges, lower first (default 100; the Vitals gauges are 1–4) | | dismissible | boolean | card, menu, form | false: no Close, and it stays after use. Gauges never close |

Anything over a limit is cut, and a line in the extension's log says what. A 13th widget removes the oldest one the player could have closed. A cmd that starts with lua or #lua is dropped (the game still gets the Action).

Client.Widget.Action

{ "id": "door", "action": "button", "index": 0, "label": "Open", "cmd": "open door" }
{ "id": "reg", "action": "submit", "label": "Submit", "values": { "name": "Ash", "cls": "mage" }, "cmd": "register Ash mage" }

action is button, option or submit; index is the button's or option's place (from 0); values holds every field of a submitted form; cmd is the command that was sent with it ("" when there was none). The command is sent first, as if typed, then the Action.

Sessions

  • Each session has its own widgets. When the link drops, its widgets go; the game shows them again after login.
  • Widgets the player cannot close (gauges, dismissible: false) are kept for the session: they come back after a reload, when the extension is enabled mid-session, and on another device attached to the session.
  • The GMCP replay on reconnect is ignored: send the widgets again after login.

Exported API

Typed in src/types.ts (shipped with the package).

const widgets = await ctx.api<WidgetsApi>('widgets');
widgets.parseShow(data);   // { id, spec, warnings } | null: a Show payload as the extension would show it
widgets.removeId(data);    // the id a Remove names ('' for all)
widgets.limits;            // the limits above
widgets.shown(sid?);       // the game ids shown in a session now, oldest first

On the host's layer a widget's id is widgets.<id> (its element has data-testid="widget-widgets.<id>").

Develop

npm install
npm run build        # dist/index.js + manifest check
npm run typecheck
npm test             # the extension in the headless host (@runmu.sh/dev/test)
npm run dev          # dev server with hot reload; open μClient with ?demo&ext-dev=<url>

In the demo, drive it from the console: __demo.gmcp('s-us-1', 'Client.Widget.Show', { id: 'hi', type: 'card', title: 'Hi', buttons: [{ label: 'Wave', cmd: 'wave' }] }), and read what was sent back in __demo.sentGmcp.