@hugpy/console
v0.1.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.
Maintainers
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 broker1. 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.
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.
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 lxd—lxc 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. |
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.
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.
