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

vibe-remote-relay

v1.1.0

Published

VibeRemote WebSocket relay — bridges host agents (desktop) and mobile clients via binary protocol v3. Ships as a NestJS module (embed) or a standalone CLI (vibe-remote-relay).

Readme

vibe-remote-relay

Korean version: README_KR.md

The VibeRemote WebSocket relay — bridges a PC agent (host) and a mobile client (mobile), forwarding the v3 binary PTY/SSH protocol between them.

Two consumer shapes:

  1. CLI / Docker — one-liner self-host
  2. NestJS library — embed as a module inside an existing Nest app

CLI usage (npm)

# one-shot
RELAY_AGENT_SECRET=$(openssl rand -base64 32) npx vibe-remote-relay

# global install
npm i -g vibe-remote-relay
RELAY_AGENT_SECRET=$(openssl rand -base64 32) vibe-remote-relay

The relay listens for WebSocket only on port 8091 by default. It does not expose an HTTP server, so put nginx / caddy in front for TLS termination.

Caddy

relay.example.com {
  reverse_proxy ws://localhost:8091
}

nginx

location /relay/ {
  proxy_pass http://localhost:8091/;
  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection "upgrade";
  proxy_read_timeout 3600s;
  proxy_send_timeout 3600s;
  proxy_buffering off;
}

Docker

git clone https://github.com/jun-young1993/vibe-remote.git
cd vibe-remote/packages/relay
cp .env.example .env
echo "RELAY_AGENT_SECRET=$(openssl rand -base64 32)" >> .env
docker compose up -d

Image: ghcr.io/jun-young1993/vibe-remote-relay:latest. It is a thin wrapper around the npm package.


NestJS library (embed)

In embed mode you take ownership of pulling values out of process.env (or ConfigService) and passing them in — the relay never reads process.env itself when it is used as a library. CLI / Docker is the only path that auto-loads env via loadRelayConfigFromEnv.

import { Module, Logger } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { RelayModule } from 'vibe-remote-relay';

@Module({
  imports: [
    ConfigModule.forRoot(),
    RelayModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: (cfg: ConfigService) => ({
        port: cfg.get<number>('RELAY_PORT', 8091),
        agentSecret: cfg.get<string>('RELAY_AGENT_SECRET', ''),
        frameSnapshot: cfg.get<string>('RELAY_FRAME_SNAPSHOT') === 'on',

        // 1.1.0 — optional
        logger: new Logger('Relay'),               // any object satisfying NestJS LoggerService
        hooks: {
          onConnect: ({ role, token }) => metrics.inc(`relay.connect.${role}`),
          onMessage: ({ opcode, byteLen }) => metrics.observe('relay.msg.bytes', byteLen),
          onDisconnect: ({ role }) => metrics.inc(`relay.disconnect.${role}`),
          onError: ({ role, err }) => sentry.captureException(err, { tags: { role } }),
        },
        verbose: {
          onMessage: cfg.get<string>('RELAY_VERBOSE_ON_MESSAGE') === 'on',
          onFrameSnapshotFeed: cfg.get<string>('RELAY_VERBOSE_ON_FRAME_SNAPSHOT_FEED') === 'on',
        },
        heartbeatIntervalMs: 30_000,
        frameSnapshotOptions: { cols: 120, rows: 40 },
        maskTokenLength: 8,
      }),
      inject: [ConfigService],
    }),
  ],
})
export class AppModule {}

Or with static config:

RelayModule.forRoot({
  port: 8091,
  agentSecret: process.env.RELAY_AGENT_SECRET!,
  frameSnapshot: true,
})

In embed mode the health endpoint (GET /relay/health) is registered automatically.


Environment variables

These are read by the CLI / Docker entrypoint only. Library users pull them themselves and pass values to forRoot/forRootAsync.

| Variable | Required | Default | Description | |---|---|---|---| | RELAY_PORT | optional | 8091 | WebSocket bind port | | RELAY_AGENT_SECRET | required | — | Shared secret for host auth (openssl rand -base64 32). Empty string → onModuleInit throws | | RELAY_FRAME_SNAPSHOT | optional | unset | Last-frame replay only enabled when set to "on" | | RELAY_HEARTBEAT_INTERVAL_MS | optional | 30000 | WebSocket ping/pong period. Must be > 0 | | RELAY_VERBOSE_ON_MESSAGE | optional | unset | "on" → every forwarded message produces a logger.debug line. Off by default to avoid log overload | | RELAY_VERBOSE_ON_FRAME_SNAPSHOT_FEED | optional | unset | "on" → every frame-snapshot feed produces a logger.debug line. Off by default | | RELAY_FRAME_SNAPSHOT_COLS | optional | 80 | xterm/headless Terminal columns. Must be > 0 | | RELAY_FRAME_SNAPSHOT_ROWS | optional | 40 | xterm/headless Terminal rows. Must be > 0 | | RELAY_FRAME_SNAPSHOT_IDLE_TTL_MS | optional | 1800000 | Snapshot idle TTL (30 min default) | | RELAY_FRAME_SNAPSHOT_GC_INTERVAL_MS | optional | 60000 | Idle-snapshot GC scan period | | RELAY_MASK_TOKEN_LENGTH | optional | 8 | First N chars of token shown in audit logs. Must be > 0 |


URL shape

ws[s]://<host>:<port>/<role>/<token>

role  = "host"   ← PC agent (must send Authorization header)
role  = "mobile" ← mobile app
token = arbitrary shared identifier (UUID v4 recommended)

The PC agent must send this header when connecting as host:

Authorization: Bearer <RELAY_AGENT_SECRET>

Mobile connects with the token only — and is rejected unless host is already OPEN for that token.


Demo with wscat

# 1) host connection
wscat -c "ws://localhost:8091/host/test-token-xyz" \
  -H "Authorization: Bearer <RELAY_AGENT_SECRET>"

# 2) from another terminal, mobile connection
wscat -c "ws://localhost:8091/mobile/test-token-xyz"

# 3) binary frames sent by either side are forwarded to the other

Security model

See SECURITY.md for details. In short:

  • TLS is terminated by an external reverse proxy.
  • Host authenticates with RELAY_AGENT_SECRET; mobile uses the token-as-secret.
  • E2EE is not implemented — the relay forwards plaintext frames.

Protocol

WebSocket binary frames, v3 opcode format. See vibe-remote-protocol for the full spec and message types.

| Opcode | Meaning | |---|---| | 0x00 | PTY raw bytes (host → mobile) | | 0x01 | APP_JSON message (bidirectional) | | 0x02 | RESIZE (mobile → host) | | 0x03 | FRAME_SNAPSHOT (relay → mobile) | | 0x04 | GRID_SNAPSHOT (host → mobile, A3 path) | | 0x05 | GRID_DIFF (host → mobile, A3 path) | | 0xFF | PROTOCOL_ERROR |


Performance notes

The relay forwards every binary frame between host and mobile. Two configuration knobs sit directly on that hot path; treat them carefully.

  • hooks.onMessage runs once per forwarded message — PTY streams can produce thousands per second. Keep the hook synchronous and cheap (counter increments, ring-buffer pushes). Do not perform HTTP / DB / disk I/O inside onMessage. If you need to ship traffic to an external system, queue inside the hook and drain on a separate timer.
  • verbose.onMessage=true calls logger.debug per message — fine for short debugging sessions, dangerous in production. A synchronous logger (winston with sync file transport, console in a slow terminal) will create backpressure. Either keep it off in production, or use a logger that samples / batches downstream.

verbose.onFrameSnapshotFeed=true has the same shape: one debug line per feed() call. Useful for reproducing terminal-rendering bugs locally, costly in steady state.

Hooks throwing is safe — the relay wraps each call in try/catch and emits a single warn line — but a throwing hook is still a bug in your code; fix it.


Migration from 1.0.0

Two relevant changes:

  1. Default logger switched from NestJS Logger to a console adapter. This removes the implicit NestJS dependency from the service internals. If you rely on NestJS log formatting, restore it with one line:
    import { Logger } from '@nestjs/common';
    RelayModule.forRoot({ ..., logger: new Logger('Relay') })
    The RelayLogger interface is structurally compatible with NestJS LoggerService, so a Logger instance drops in with no adapter.
  2. RelayConfig.agentSecret was already required; the empty-string error message changed from RELAY_AGENT_SECRET is not set to RelayConfig.agentSecret is required (empty). If you grep error logs for the old wording, update it.

Everything else is additive — new optional fields (hooks, verbose, heartbeatIntervalMs, frameSnapshotOptions, maskTokenLength) default to the 1.0.0 behavior when omitted.


License

MIT — see LICENSE.


Related