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-puppets

v1.2.0

Published

Remote puppet viewpoints: a feed, scene and command line per puppet, with unread counts on the tab, driven by GMCP Client.Puppets.* or another extension's API.

Readme

Puppets (@runmu.sh/ext-puppets, id puppets)

Remote NPC viewpoints. Each puppet gets a feed, its scene, and a command line that acts as that NPC. Install it from Extensions → Discover in μClient (puppets), then enable it per world. Its settings are under Settings → Extensions → Puppets. While it is not off it sends Core.Supports.Add ["Client.Puppets 1"].

Needs μClient extension API 1.12 or later. Capabilities: send-commands (the command line and its fallback) and files (Save).

GMCP contract

Every message has a JSON schema in schema/, declared in package.json. The host rejects a payload that doesn't match (the session shows an error) before Puppets sees it.

| Dir | Package | Payload | |---|---|---| | S→C | Client.Puppets.Manifest | { "puppets": [{ "npc_id", "slot"?, "name"? }] }: the whole set; puppets missing from it are dropped. Sorted by slot, then npc id. | | S→C | Client.Puppets.Feed | { "npc_id", "lines": [{ "body", "revision"?, "action_id"?, "news"? }] }. A news: "room" line counts as unread unless that puppet is open. A body containing <span, <div, <p or <br is treated as HTML (sanitised), anything else as text with ANSI colours. | | S→C | Client.Puppets.Scene | { "npc_id", "revision"?, "room"?: { "name"?, "desc"? }, "occupants"?: [..], "exits"?: [..] } | | S→C | Client.Puppets.Sync | { "npc_id", "revision"?, "lines": [...], "scene"?: {...} }: replaces the buffer (the answer to Resync) | | C→S | Client.Puppets.Resync | { "npc_id", "revision" }, sent when a puppet is opened, with the last revision held | | C→S | Client.Puppets.Command | { "npc_id", "cmd" }: the cmd action, via gmcp by default, falling back to the command @puppet {npc} = {cmd} when GMCP can't be sent | | C→S | Client.Puppets.Add | { "npc_id" }: the add action (another extension's "Add puppet"), via gmcp, no command fallback unless one is set. The game answers with a new Manifest. |

npc_id may be a number or a string; Puppets always sends it as a string. The buffer holds the last 300 lines per puppet. The unread total for a session is shown as the badge on the Puppets tab (the title stays "Puppets"), and the panel adds itself, unfocused, the first time a Manifest or Feed arrives. When Puppets is enabled late, the host replays the last Manifest; replayed Feeds are ignored so nothing is counted twice.

Each action's route (gmcp, command, ext, none) and command template can be changed per world under Settings → Extensions → Puppets → Actions (keys ext.puppets.puppets.action.cmd.via|cmd and ….add.via|cmd, as in 1.1).

Settings

| Key | Default | | |---|---|---| | ext.puppets.puppets.enabled | auto | Show panel: auto (when the game sends puppets), on, off. Provided by μClient for the panel. | | ext.puppets.puppets.source | gmcp | Driven by: gmcp, api (another extension), or both. |

API (await ctx.api<PuppetsApi>('puppets'), types in src/types.ts, @runmu.sh/ext-puppets/types)

Add "dependsOn": ["puppets"] to your manifest, or catch the rejection when Puppets isn't enabled in the world. What you register through the API is removed when your extension is disabled. sid defaults to the active session.

| Method | | |---|---| | enable(mode, worldId?) | off / auto / on | | open(npcId?) | open the panel, or a puppet's viewpoint (marks it read, asks for a resync) | | set('manifest', {puppets}, sid?), set('scene', npcId, scene, sid?), push('feed', {npc_id, lines}, sid?), sync({...}, sid?) | drive Puppets yourself (source api or both); same shapes as the GMCP messages | | get('manifest', sid?), get('feed', npcId, sid?), unread(sid?) | read the state | | onAction('cmd' \| 'add', fn) | see a command or add before it is sent; return true to handle it yourself | | onRequest('resync', fn) | answer a resync yourself instead of sending Client.Puppets.Resync | | configure({enabled?, source?, actions?}, worldId?) | set modes and action routes | | 1.2 add(npc, sid?) → Promise<'sent'\|'handled'\|'hidden'\|'duplicate'\|'present'> | ask the game to make npc (an id, or {npc_id\|id, name?}) a puppet. 'present': already one, nothing sent. 'hidden': Puppets is off there, the action is hidden, there is no session, or GMCP was refused and no fallback is set. | | 1.2 has(npcId, sid?) | is the NPC already a puppet (Underspire's "In puppets") | | 1.2 canAdd(sid?) | whether add would be sent (Puppets not off, add action not hidden) | | 1.2 watch(fn, sid?) → dispose | called now and on every manifest change with the session's puppets |

import type { PuppetsApi } from '@runmu.sh/ext-puppets/types';
const puppets = await ctx.api<PuppetsApi>('puppets').catch(() => null);
if (puppets?.canAdd(sid) && !puppets.has(npc.id, sid)) await puppets.add({ npc_id: npc.id, name: npc.name }, sid);

What it looks like

The list is made of .sh-row lines: the slot (P1, gold), the name, #<npc id> (small, faint), the room (dim, at the right) and a square gold badge with the unread count (99+ above 99). A puppet with unread lines is .hot, so its ▸ marker is lit. With no puppets it shows NO REMOTE PUPPET VIEWPOINTS. The terminal view has a head over an accent rule with [ BACK ], the slot, name, #id and room, then [ SAVE ] (download the buffer as plain text, puppet-<npc>-<YYYY-MM-DD-HH-MM-SS>.txt) and [ CLEAR ] (asks first). Below is the scrolling feed, then a small scene summary (room, occupants, exits), with LOADING VIEWPOINT while it resyncs. At the bottom is a P1> command line with the placeholder ACT AS , focused when the puppet is opened.

Test ids: puppets, puppets-list, puppets-empty, puppet-unread, puppet-back, puppet-save, puppet-clear, puppet-feed, puppet-scene, puppet-input.

Develop

npm run build && npm run typecheck && npm test (headless host from @runmu.sh/dev/test, with happy-dom).