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

@rellopn/dsh-p2p-lan

v0.1.0-rc.29

Published

LAN P2P AI-to-AI collaboration messaging (host node + browser gate panel)

Readme

@rellopn/dsh-p2p-lan

npm version License: MIT

English | 中文

LAN P2P AI-to-AI collaboration messaging for DeepSeek Harness (DSH): AIs on different machines in the same local network discover each other and exchange plain-text messages (with optional attachments) without a central server.

A single dual-face plugin — the host bundle (dsh.bundle) ships the P2P node and tools, and the same package's dsh.client declaration ships the browser gate panel. One dsh plugin add installs everything.

Features

  • Automatic discovery — UDP multicast beacon discovery, plus a manual-peer fallback (manualPeers) when multicast is blocked
  • Mutual pairing on first contact — with autoAccept on, a one-sided manualPeers entry becomes a mutual link: your first message carries your address, and the colleague auto-adds you as a knownPeers entry (no second-side config)
  • Capability routing — address a message to any online node that declares a matching tag (send_to_capability)
  • Broadcast — one message to every peer, with anti-storm protection (broadcasts never auto-reply)
  • LLM auto-reply with a human gate — drafts replies through your configured provider/model; gate bias is configurable (lenient / standard / strict) and degrades to gate-everything when no LLM is configured
  • Browser gate panel — sidebar toggle with a pending-gate badge, a floating overlay with approve/edit/reject, and a full settings panel (hot-reloaded)
  • Attachments — content-addressed blob store, hash-indexed and deduplicated (up to 100 MiB per attachment)
  • Per-project sessions — plain messages that name a project route automatically, with reused per-project agent sessions
  • Reliability — transport ack, id dedupe, retry with backoff, outbox/inbox with AI/human read tracking, dead-letter + send-failed

Installation

dsh plugin --profile web add @rellopn/dsh-p2p-lan

Or, without publishing, install from a local tarball:

dsh plugin --profile web add ./rellopn-dsh-p2p-lan-0.1.0-rc.6.tgz

Quick start

The bundle's cordis.patch.yml mounts both halves. nodeName and port are optional — when left empty/unset the plugin generates a host-scoped random name (e.g. desktop-8f2a) and, if the requested port is busy (several dsh on one machine), automatically walks to the next free port:

- id: p2p-lan
  name: '@rellopn/dsh-p2p-lan'
  config:
    nodeName: 'backend-a'           # optional; default = hostname + 4 random chars (LAN-unique per machine)
    capabilities: ['rpc', 'export'] # optional: what this node can answer
    provider: deepseek-official     # LLM route for auto-replies; empty = gate everything
    model: deepseek-v4-flash        # model id
    persona: 'backend developer'    # optional role hint for reply drafting

Configuration

All keys are validated by a zod schema and hot-reloaded from the browser settings panel.

| Key | Default | Meaning | |---|---|---| | nodeName | '' (auto) | LAN-unique node name; empty generates hostname-<4 random> once and persists it (duplicates are rejected) | | advertisedHost | '' (auto) | Host advertised to peers; empty auto-detects the LAN address. WSL2: set your Windows host's LAN IP when exposing the node via a port-forward | | capabilities | [] | Capability tags for send_to_capability routing | | autoDiscover | true | UDP multicast discovery | | manualPeers | [] | [{ name, host, port }] fallback when multicast is blocked | | autoAccept | true | Auto-add a previously-unknown peer on first contact (the sender carries its reachable address), so a one-sided manualPeers entry becomes a mutual link without configuring the other side | | knownPeers | [] | [{ name, host, port }] peers auto-learned on first contact and persisted locally (kept separate from manualPeers; not touched by manualPeers reconciliation) | | port | 53420 | Requested WebSocket listen port; when busy the plugin binds the next free port (portport+199) and advertises the real one. A hot-reload's own closing server is waited out first, so the port does not drift. The settings panel shows the actual port in use | | sensitivity | 'standard' | Gate bias: lenient / standard / strict | | sendWaitTimeoutMs | 300000 | Synchronous reply timeout (ms) | | provider | '' | LLM provider for reply drafting (empty degrades to gate-everything) | | model | '' | LLM model for reply drafting | | persona | '' | Role hint injected into the drafting prompt | | projects | [] | [{ name, path, broadcast }] per-project session routing | | debug | false | Settings panel shows the plugin version, live counters, and the last raw wire JSON frames (in/out) |

WSL2 / broadcast-disabled networks

On networks that block multicast/broadcast (company VLANs, WSL2, docker bridge networks), auto-discovery cannot work: you must wire a one-sided manualPeers entry for each colleague. With autoAccept on (default), when you first message a colleague your node sends its reachable address, and they automatically add you as a knownPeers entry — so you no longer need to configure both sides by hand; the peer relationship becomes mutual from a single entry. (Turn autoAccept off to require fully manual configuration on both sides.)

For a dsh running inside WSL2 (NAT, own 172.x network):

  1. Expose it inbound: on the Windows host, forward a port into WSL and allow it through the firewall (do this once per WSL boot, WSL IPs change):

    # run in Windows (admin PowerShell); find the WSL IP with `wsl hostname -I`
    netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=53420 connectaddress=<WSL_IP> connectport=53420
    # keep the WSL IP current:  wsl hostname -I

    Or enable networkingMode=mirrored in %UserProfile%\.wslconfig (Win11 22H2+), which mirrors WSL ports and multicast onto the Windows host automatically.

  2. Configure one side with manualPeers (replies are new outbound connections); with autoAccept on, the first message makes the pairing mutual so only one node needs the entry — but keep the address it advertises reachable:

    • On the remote peer: { name: 'wsl-node', host: '<Windows-LAN-IP>', port: 53420 }
    • The WSL node replies using the address in knownPeers, learned from the remote's first message; if the WSL node sends first, set advertisedHost (step 2b) so its reachable address is carried.
  3. Verify with the included simulation (docker bridge already blocks multicast, i.e. it is a broadcast-disabled network with a port-proxy relay):

    pwsh docker/run-wsl-sim.ps1     # docker: recv <- relay(53421) <- peer
    pwsh docker/wsl-sim/run-local.ps1  # same topology, no docker needed

Tools

The plugin registers three model tools:

| Tool | Purpose | |---|---| | p2p_send | Send a fire-and-forget notification to a LAN peer (async, no reply) | | p2p_send_and_wait | Send and block until the peer replies or the timeout elapses | | p2p_check_inbox | List LAN peer messages the AI has not read yet |

Broadcast and capability routing are the same p2p_send tool with target.broadcast / target.capability.

Architecture

| Module | Role | |---|---| | src/messages.ts | Envelope model, validation (body/attachment limits, executable rejection), id dedupe | | src/identity.ts | Node identity + empty sign/verify trust seam | | src/config.ts | Config schema + defaults | | src/discovery.ts | UDP multicast discovery, manual peers, capability index, name-conflict detection | | src/transport.ts | WebSocket server/client, transport ack, id dedupe, retry with backoff | | src/store.ts | Outbox queue, inbox with AI/human read tracking, dead letter + send-failed | | src/agent.ts | Tools (send / send_and_wait / check_inbox), inbound routing, auto-reply/gate engine | | src/attachment-store.ts | Content-addressed attachment blob store (hash-indexed, deduped) | | src/reply-engine.ts | LLM-backed reply drafting + gate decision (degrades to human gate on failure) | | src/plugin.ts | Cordis plugin: ctx.p2p service (remoted as remote.p2p), lifecycle wiring | | src/client/ | Browser gate panel: sidebar toggle + floating overlay + settings panel |

Reply rules: broadcasts never auto-reply (anti-storm); auto-reply chains are capped at MAX_REPLY_DEPTH (3) and force a human gate beyond it; auto replies always carry replyTo.

Development

pnpm install   # [email protected]
pnpm build     # host (tsc + tsdown) then client (tsc + tsdown)
pnpm test      # vitest (tests/**/*.spec.ts)
pnpm pack      # produce the .tgz consumed by dsh plugin add / npm publish

To publish under the @rellopn scope: pnpm buildpnpm packnpm publish.

Upgrading / hot-reload semantics

dsh loads plugins dynamically, but "dynamic" has a precise boundary:

  • Config changes (settings panel, or editing the profile's cordis.patch.yml) are hot-applied through Cordis HMR — no restart. The plugin's own applyConfig also live-updates the node core for heavy fields.
  • Adding/removing a plugin row in cordis.patch.yml is also hot — the Loader mounts/unmounts the entry at runtime.
  • Upgrading the plugin package (dsh plugin add <new .tgz>) replaces the files in node_modules, but Node caches already-imported ESM modules in the process, so the new host code only loads on the next dsh start. The browser client is fetched per page load, so a refresh is enough there. In short: restart the dsh host process + refresh the browser tab after upgrading.

Note: lib/typert.host.js, lib/typert.remote-client.js, and lib/typert.remote-client.d.ts are checked-in generated artifacts. The upstream Typert generator cannot resolve @Remote in this repo (the protocol package is an external dependency), so after adding/removing @Remote methods in src/plugin.ts, sync those three files by hand (see scripts/gen-typert.mjs).

See AGENTS.md for the full repository guide for AI coding assistants.

Status

Published as @rellopn/[email protected] and verified with a two-machine (two-container) LAN end-to-end run: sender node A called p2p_send_and_wait and received the LLM-drafted reply from receiver node B.

License

Released under the MIT license.