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

@hugpy/console

v0.3.1

Published

Portable terminal / file-browser / keeper console for the web — a framework-agnostic mountConsole() module plus a reusable PTY broker. Embeds into any app.

Downloads

49

Readme

station-console

A portable terminal / file-browser / LLM-chat console you can drop into any web app. It is the console UI from the LXD station manager, extracted into a mountable JS module plus a reusable backend broker so other applications can embed it without the vm_mgr station chrome.

station-console/
  ui/
    station-console.js   ESM module — export mountConsole(el, opts)
    style.css            all styles scoped under .sc-root (no host collisions)
    vendor/              xterm.js + fit/canvas addons + xterm.css
  server/
    pty_broker.py        aiohttp broker: ws PTY + fs REST + optional LLM proxy
  example/index.html     demo host page that mounts the module
  install.sh             systemd install of the broker

1. The UI module

<link rel="stylesheet" href="ui/vendor/xterm.css">
<link rel="stylesheet" href="ui/style.css">
<script src="ui/vendor/xterm.js"></script>
<script src="ui/vendor/xterm-addon-fit.js"></script>
<script src="ui/vendor/xterm-addon-canvas.js"></script>
<div id="console" style="height:600px"></div>
<script type="module">
  import { mountConsole } from "./ui/station-console.js";
  const api = mountConsole(document.getElementById("console"), {
    backend: "https://my-broker:8801",  // '' = same origin
    vm:      "local",                   // target the broker exposes
    token:   "…",                       // optional API token
    namespace: "app1",                  // localStorage prefix (isolate multiple mounts)
    features: { files: true, llm: true, keeper: true, composer: true },
  });
</script>

xterm.js and the fit addon must be loaded before the module (the canvas addon is optional — it falls back to the DOM renderer). The module owns only the DOM inside the element you pass; the host app keeps its own chrome.

Bundler / npm path

The script-tag example above is the zero-build path — it needs nothing from npm besides @hugpy/console itself, which is why @xterm/xterm, @xterm/addon-fit, and @xterm/addon-canvas are declared as optional peer dependencies rather than hard ones: the vendored UMD copies under ui/vendor/ already satisfy the module at runtime via window.Terminal / window.FitAddon / window.CanvasAddon. If you're bundling (webpack, vite, esbuild, …) instead, install xterm yourself and pass the classes in directly so the module never touches window:

npm install @hugpy/console @xterm/xterm @xterm/addon-fit @xterm/addon-canvas
import { mountConsole } from '@hugpy/console';
import '@hugpy/console/style.css';
import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import { CanvasAddon } from '@xterm/addon-canvas';
import '@xterm/xterm/css/xterm.css';

const api = mountConsole(document.getElementById('console'), {
  backend: 'https://my-broker:8801',
  vm:      'local',
  token:   '…',
  namespace: 'app1',
  features: { files: true, llm: true, keeper: true, composer: true },
  xterm: { Terminal, FitAddon, CanvasAddon },
});

xterm.Terminal and xterm.FitAddon are required if you use this path — mountConsole throws immediately if neither they nor the window globals are present. xterm.CanvasAddon is optional here too; omit it (or pass xterm: { Terminal, FitAddon }) and the terminal falls back to the DOM renderer, same as the script-tag path. Pick the script-tag path for a zero-build host page; pick this path when your app already has a bundler and you'd rather not vendor a second copy of xterm.

API returned by mountConsole

| method | description | |---|---| | openPane(spec) | open a pane. spec: {type:'shell'} or {type:'keeper', backend, model, skipPerms}. Returns the pane id. | | closePane(id) | close a pane (kills its server session). | | setTarget(name) | switch the whole console to another target (tears down panes, reboots from that target's saved layout). | | getTarget() | current target name. | | setView('term'\|'files'\|'llm') | switch the active view. | | on(event, fn) | subscribe; returns an unsubscribe fn. Events: paneOpen, paneClose, paneExit, targetChange. | | destroy() | close sockets, detach listeners, clear the element. |

Retained features: keeper terminal with model selection, optional --dangerously-skip-permissions, split panes, copy-on-select, the read/write file browser, the streaming LLM chat, and the multiline composer. The winsize-before-spawn fix is built in — a full-screen TUI keeper paints its first frame at the real pane geometry, not a stale 24×80.

File browser: search + navigation

The file browser (features.files) adds three things on top of the read/write listing/editor:

  • Content search — the search row greps a literal substring through the files under the current folder (recursive) and lists file:line — matched text hits; clicking a hit opens the file in the editor and scrolls near the line. Clear returns to the directory listing. It skips binary files and noise directories (.git, node_modules, __pycache__, …), caps at 500 hits / 1 MB per file, and is time-bounded — see the fs/search broker route below.
  • Breadcrumb path — the current path renders as clickable ancestor segments (root included); each jumps there. The toggle swaps in an editable text input for typing/pasting a path (Enter navigates, Esc cancels).
  • Recent folders — the 🕘 dropdown lists the last ~8 distinct folders visited, most-recent first; picking one navigates to it. Recents are stored in localStorage, keyed per namespace/target.

Model chat replies are rendered as plain text via textContent (never innerHTML), so model output cannot inject markup.

2. The broker

Speaks exactly the protocol the module expects. The target resolver is the portable seam — it decides how {name} maps to a PTY/filesystem:

  • --resolver local — a plain shell + the host filesystem. Works anywhere.
  • --resolver lxdlxc exec <name> as an unprivileged dev user (LXD VMs).
# standalone, any host, open in dev:
python3 server/pty_broker.py --resolver local --token "$TOK" --cors-origin '*'

# LXD parity with the original station console:
python3 server/pty_broker.py --resolver lxd --port 8801

| env / flag | meaning | |---|---| | --token / STATION_BROKER_TOKEN | API token (X-Console-Token header or ?token=). | | --set-password | print a pbkdf2 hash; set STATION_BROKER_PASSWORD_HASH to require a login. | | --cors-origin / STATION_BROKER_CORS_ORIGIN | * or comma-separated origins — required for cross-origin embeds. | | --llm-base / STATION_BROKER_LLM_BASE | OpenAI-compatible gateway base URL; enables /api/llm/models + /chat. Omit → chat shows "no models". | | --cert / --key | enable TLS. | | STATION_BROKER_DIRECTIVE | global keeper directive file (default /opt/llm-station/directive.md). | | STATION_BROKER_KEEPER_SOCK | tmux socket for persistent keepers (default console). Use a distinct socket to isolate keepers from another console on the same host. | | STATION_BROKER_LOCAL_NAME | station name the local resolver advertises (default local); set to label the host, e.g. hugpy. |

Content search. GET /api/stations/{name}/fs/search?path=<rel>&q=<text> greps the literal substring q (case-insensitive; add &case=sensitive for exact case) through the text files under path, returning {"results":[{"file","path","line","text"}], "truncated":bool}. It runs through the same resolver seam as fs/list — reading the host for local, inside the VM for lxd — and is confined to the same sandbox root: a .. or absolute path that resolves outside the root is refused with 400 before any file is touched. The query and path are passed as argv (never interpolated into a shell), so metacharacters are inert. Binary files, .git/node_modules/ __pycache__-style noise dirs, and files ≥1 MB are skipped; results are capped at 500 hits and the whole search is time-bounded.

Keeper persistence. A Claude keeper (+🤖) runs inside a tmux session, so it survives a broker restart — the PTY we spawn is just a tmux client; the keeper keeps running (in the VM for lxd, on the host for local) and reconnecting reattaches. Its system prompt layers the global directive above, then a per-target file — /srv/share/projects/<name>/.keeper-directive.md (lxd) or <home>/.keeper-directive.md (local) — each applied only if present.

To add your own target type (ssh, docker, k8s pod…), implement a resolver class (list_targets, safe_path, shell_argv, run, pull, keeper_argv) and register it in main().

3. One-command install

sudo ./install.sh --resolver local --port 8801 --cors-origin 'https://my-app.example'

Generates an API token (printed once), a self-signed TLS cert, and a systemd unit (station-console-broker.service). The broker also serves ui/ at /ui, so a host page can import the module from the same origin.

0.3.0 — fleet-console fold-back

0.3.0 converges the terminal-tab UX that was proven this epoch in the multi-host fleet console (blackbird console-ui) back into this single-host package. Only the parts that map to a one-host console were folded in — per-VM grouping, board features, and power controls were intentionally left out.

  • Keeper-primary pane tabs. Keeper terminals (+🤖 / openPane({type:'keeper'}), tagged by pane.spec.keeper) now lead the tab strip and render primary (bright + bold); shell tabs follow and render secondary (dim). The keeper is the operator's workspace and must never read as a mere peer of the shell — the same semantics the fleet console's unified tab bar settled on. Per-kind numbering (🤖1…, sh1…) keeps labels sequential regardless of open order.
  • Mobile tab strip. Under a narrow-viewport media query (≤640px) the .pane-tabs strip becomes a hidden-scrollbar, touch-scrollable horizontal strip, and the toolbar buttons wrap to a new row instead of scrolling off the right edge (the fleet-console mobile bug where +🤖 / view buttons became unreachable). The console never forces page-level horizontal overflow.
  • Wrap / overflow guards. Long filenames, paths, search-hit text (file browser) and long chat messages / command output (LLM chat) get the fleet console's min-width:0 / overflow-wrap:anywhere treatment so an unbreakable token can't force a horizontal scrollbar or widen the panel.

Provenance: fleet console console-ui commits 3e85e78..9ca8ed1.

Relation to vm_mgr

The original console at vm_mgr/console/ is unchanged and still runs the full station manager (VM tabs, lifecycle, provisioning). This package is the reusable extraction of its console core; vm_mgr can later be re-pointed at the module, but nothing here modifies it.