All docs / npm packages

@decentnetwork/beagle

P2P chat, file transfer and calls for regular users on the Decent Network, as a CLI and local desktop app. No admin privilege required.

Version
0.1.132 · npm
Platform
Node.js 20+ · desktop
Source
Source repository not public yet
npm install -g @decentnetwork/beagle

@decentnetwork/beagle is the Beagle chat client for computers: a beagle command that runs a Carrier peer and serves the Beagle web UI on http://localhost:8766 for friends, messages, files, groups and calls. It needs no account, server or admin privilege. The identity is a keypair in a local directory. The package also exports the pieces that make that work: the local HTTP server, the backend selector that either runs a peer in-process or hands requests to a running AgentNet daemon, and the daemon's IPC client.

When to use it

  • You want a ready chat client on macOS, Linux or Windows to test your own app against. A peer you build with @decentnetwork/peer can friend and message a Beagle user.
  • You want to drive a Beagle identity from Node with the same request shapes the UI uses (friends-list, chat-send, file-send, and so on) through openPeerHost().
  • You run AgentNet and want a chat UI on the same identity. Beagle detects a running daemon and uses it instead of starting a second peer on the same keypair.
SiblingUse it for
@decentnetwork/peerBuilding your own app on the protocol
@decentnetwork/beagle-uiThe UI bundle this package serves, to host it in your own page
@decentnetwork/beagle-connect"Sign in with Beagle" from a website
@decentnetwork/lanA virtual LAN (TUN, needs root) on the same identity

Install

npm install @decentnetwork/beagle@0.1.132
npx beagle --help

The project's own instructions install it globally (npm install -g @decentnetwork/beagle). A local install plus npx works the same way and does not touch global state.

Quickstart

1. Run the client

mkdir beagle-demo && cd beagle-demo
npm init -y
npm install @decentnetwork/beagle@0.1.132
npx beagle --config-dir ./beagle-home --port 18766     # Ctrl+C to stop
# in another terminal:
curl -s http://127.0.0.1:18766/api/ping
curl -s http://127.0.0.1:18766/api/state

Then open http://localhost:18766 in a browser. Leave out --config-dir to use ~/.agentnet, the same identity AgentNet uses on that machine. Leave out --port to use 8766.

Verified output (npm @decentnetwork/beagle@0.1.132, Node 24.3.0, macOS arm64, 2026-11-06):

$ npx beagle --config-dir ./beagle-home --port 18766
[Carrier] INFO: identity KKAoGHgwvz3ng139afcE8GsoLedFAjL5pChurDkLWzn9JXWTYrgL
beagle 0.1.132 — backend: embedded (embedded peer, new identity at …/beagle-demo/beagle-home/carrier/keypair.json)
Beagle UI on http://localhost:18766

$ curl -s http://127.0.0.1:18766/api/ping
{"ok":true}

$ curl -s http://127.0.0.1:18766/api/state
{"me":{"userid":"9LFt2mAg2cnqgPmhEF284Pf6una4ioeG43exrpBek3dL","address":"KKAoGHgwvz3ng139afcE8GsoLedFAjL5pChurDkLWzn9JXWTYrgL","name":"","ip":null,"installed":false},"friends":[],"pending":[]}

Give your address (the long string) to someone who wants to friend you. The userid is the public key that identifies you to friends you already have.

2. Drive a Beagle identity from Node

openPeerHost() picks a backend. With force: "embedded" it runs a Carrier peer in your process. The returned host answers the same request objects the UI sends:

// quickstart.mjs
// Open a Beagle peer host in-process (no root, no daemon) and talk to it
// with the same IPC-shaped requests the Beagle UI uses.
import { resolve } from "node:path";
import { openPeerHost, decentlanCarrierDir, ipcSocketPath, probeDaemon } from "@decentnetwork/beagle";

const configDir = resolve(process.argv[2] ?? "./beagle-demo");
const dataDir = decentlanCarrierDir(configDir);
console.log("daemon socket would be:", ipcSocketPath(dataDir));
console.log("decentlan daemon running:", await probeDaemon(dataDir));

const { host, why } = await openPeerHost({
  configDir,
  force: "embedded",
  bootstrapNodes: [
    { host: "13.58.208.50", port: 33445, pk: "89vny8MrKdDKs7Uta9RdVmspPjnRMdwMmaiEW27pZ7gh" },
    { host: "18.216.102.47", port: 33445, pk: "G5z8MqiNDFTadFUPfMdYsYtkUDbX5mNCMVHMZtsCnFeb" },
    { host: "54.193.141.205", port: 33445, pk: "7TfZWZNV8vnBxxWzJXuvKgX2QyKkLpg2oXx3LQ5tg8LW" },
  ],
  expressNodes: [{ host: "lens.beagle.chat", port: 443, pk: "ECbs4GxwGzxGerNkmqDJFibEmevu8jAXqAZtikccvD95" }],
  nickname: "docs-demo",
  autoAcceptFriends: false,
});
console.log("backend:", host.kind, "-", why, "| virtual LAN:", host.hasVirtualLan);

const diag = await host.call({ op: "diag" });
console.log("userid:", diag.ok && diag.data.identity.userid);
console.log("address:", diag.ok && diag.data.identity.address);
const friends = await host.call({ op: "friends-list" });
console.log("friends-list:", JSON.stringify(friends));
await host.stop();
console.log("stopped");
process.exit(0);

Verified output (npm @decentnetwork/beagle@0.1.132, 2026-11-06, peer debug lines trimmed):

$ node quickstart.mjs
daemon socket would be: …/beagle-demo/carrier/daemon.sock
decentlan daemon running: false
[Carrier] INFO: identity GMGdcixVnhPjeJvbQGGC9F3q5k9C5NVgq6QSKD4UbYJMK3HXHNQ2
backend: embedded - embedded peer, new identity at …/beagle-demo/carrier/keypair.json | virtual LAN: false
userid: 7z3jqugUoXtcSeH6UBKiqV18613bQ3PA8pjWGuq3o6HY
address: GMGdcixVnhPjeJvbQGGC9F3q5k9C5NVgq6QSKD4UbYJMK3HXHNQ2
friends-list: {"ok":true,"data":{"friends":[]}}
stopped

Never open two peers on the same keypair at once. Stop beagle before running this script against the same config directory, and the other way round.

API reference

Exports from @decentnetwork/beagle (dist/index.d.ts).

Backend selection

type BackendKind = "daemon" | "embedded";

interface PeerHost {
  readonly kind: BackendKind;
  readonly hasVirtualLan: boolean;          // true only for the daemon backend
  call(req: IpcRequest): Promise<IpcResponse>;
  stop(): Promise<void>;
}

function openPeerHost(opts: OpenPeerHostOptions): Promise<{ host: PeerHost; why: string }>;
function defaultConfigDir(): string;                 // $AGENTNET_CONFIG_DIR or ~/.agentnet
function decentlanCarrierDir(configDir: string): string;  // carrier.dataDir from config.yaml, else <configDir>/carrier

OpenPeerHostOptions:

FieldTypeNotes
configDirstringWhere the identity lives
bootstrapNodes{ host; port; pk }[]Carrier bootstrap nodes
expressNodes?{ host; port; pk; tls? }[]Offline-message relays
nickname?, statusMessage?stringAdvertised to friends (embedded backend)
autoAcceptFriends?booleanAccept incoming friend requests automatically (embedded backend)
onAutoAcceptChange?(enabled: boolean) => voidPersist a runtime toggle
peerVersion?stringReported by an embedded host's ping
force?BackendKindSkip auto-detection
waitForDaemonMs?numberWith force: "daemon", how long to wait for the socket (default 60 s)

Without force, a running AgentNet daemon for this identity wins. Otherwise Beagle runs the peer itself.

Requests

type IpcOp =
  | "ping" | "diag"
  | "friend-request" | "friends-pending" | "friends-accept" | "friends-reject"
  | "friends-list" | "friend-remove" | "friend-set-alias" | "friends-autoaccept"
  | "set-profile" | "sign"
  | "chat-send" | "chat-history" | "chat-log-local" | "chat-mark-read" | "chat-retry"
  | "file-send" | "file-delete" | "file-cancel" | "file-retry" | "file-log-local"
  | "call-signal" | "call-poll";

interface IpcRequest { op: IpcOp; address?; hello?; userid?; text?; alias?; name?; path?; since?; limit?; … }
type IpcResponse = { ok: true; data?: Record<string, unknown> } | { ok: false; error: string };

Talking to an AgentNet daemon directly

function ipcCall(dataDir: string, req: IpcRequest, timeoutMs?: number): Promise<IpcResponse>;
function ipcSocketPath(dataDir: string, platform?: NodeJS.Platform): string; // <dataDir>/daemon.sock, or a named pipe on Windows
function probeDaemon(dataDir: string, timeoutMs?: number): Promise<boolean>; // connects, so it answers on every platform
function daemonSocketMayExist(dataDir: string): boolean;                     // sync hint only

Every request is one line of JSON over the socket, and the daemon answers with one line of JSON.

Local server

function startBeagleServer(opts: BeagleServerOptions): { stop: () => void };

BeagleServerOptions requires call (usually host.call) and routesPath. Optional fields include listenHost, listenPort, downloadsDir, messagesPath, greetingsPath, doraRosterPath, callIce (keypair and bootstrap nodes for minting TURN credentials), bridgeWs, lanHost, onRestart and log. The CLI in dist/cli.js is the reference wiring.

The server's /api/* routes (for example /api/state, /api/ping, /api/diag, /api/friends-list, /api/chat-send, /api/file-send) and its /connect page exist for the bundled UI and for Sign in with Beagle. They are not documented as a stable public API.

Configuration

CLI flags

beagle [options]
  --port <n>         HTTP port (default 8766, or $BEAGLE_PORT)
  --host <addr>      bind address (default 127.0.0.1)
  --config-dir <p>   identity/config dir (default ~/.agentnet, or $AGENTNET_CONFIG_DIR)
  --dora-dir <p>     dora roster dir, if this machine runs a dora
  --bridge <wss>     bridge whose roster to show (default wss://bridge.beagle.chat/relay-ws)
  --backend <k>      force 'daemon' or 'embedded' (default: auto)
  --wait-daemon <s>  with --backend daemon, wait this long for it (default 60)
  -h, --help

Files in the config directory

PathContents
config.yamlOptional. If present (AgentNet's file), Beagle reads carrier.bootstrapNodes, carrier.expressNodes, node.name, node.statusMessage and friends.autoAccept from it. Otherwise it uses the bootstrap and express lists shipped in the package's config/bootstrap-nodes.yaml, and auto-accept defaults to on.
carrier/keypair.jsonThe Carrier identity (carrier.dataDir overrides the location)
carrier/daemon.pid, carrier/holder.http-portIdentity lock and the UI port of the process holding it
messages.json, downloads/, file-resume/Chat history, received files, partial transfers
routes.yaml, friend-greetings.json, beagle-exit-state.jsonExit-routing table, kept friend-request greetings, exit probe state

Environment variables

VariableEffect
BEAGLE_PORTDefault HTTP port when --port is not given
AGENTNET_CONFIG_DIRDefault config directory
AGENTNET_LOG_LEVELdebug / info / warn / error
BEAGLE_ENS_GATEWAYGateway for *.beagles.eth names (default https://ens-gateway.beaglechat.workers.dev)

Platforms

  • Node.js >= 20 (engines). Verified with Node 24.3.0 on macOS (arm64).
  • macOS, Linux and Windows. No root or native build step. The embedded backend is pure JavaScript.
  • The UI runs in any current browser. Calls need WebRTC in the browser.
  • Runtime dependencies: @decentnetwork/peer, @decentnetwork/peer-webrtc, @decentnetwork/beagle-ui, @decentnetwork/chat-components, @decentnetwork/lan, js-yaml, ws and yargs.

Version, source and changelog