@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.
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) throughopenPeerHost(). - 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.
| Sibling | Use it for |
|---|---|
| @decentnetwork/peer | Building your own app on the protocol |
| @decentnetwork/beagle-ui | The UI bundle this package serves, to host it in your own page |
| @decentnetwork/beagle-connect | "Sign in with Beagle" from a website |
| @decentnetwork/lan | A virtual LAN (TUN, needs root) on the same identity |
Install
npm install @decentnetwork/beagle@0.1.132
npx beagle --helpThe 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/stateThen 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":[]}}
stoppedNever 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>/carrierOpenPeerHostOptions:
| Field | Type | Notes |
|---|---|---|
configDir | string | Where the identity lives |
bootstrapNodes | { host; port; pk }[] | Carrier bootstrap nodes |
expressNodes? | { host; port; pk; tls? }[] | Offline-message relays |
nickname?, statusMessage? | string | Advertised to friends (embedded backend) |
autoAcceptFriends? | boolean | Accept incoming friend requests automatically (embedded backend) |
onAutoAcceptChange? | (enabled: boolean) => void | Persist a runtime toggle |
peerVersion? | string | Reported by an embedded host's ping |
force? | BackendKind | Skip auto-detection |
waitForDaemonMs? | number | With 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 onlyEvery 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, --helpFiles in the config directory
| Path | Contents |
|---|---|
config.yaml | Optional. 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.json | The Carrier identity (carrier.dataDir overrides the location) |
carrier/daemon.pid, carrier/holder.http-port | Identity 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.json | Exit-routing table, kept friend-request greetings, exit probe state |
Environment variables
| Variable | Effect |
|---|---|
BEAGLE_PORT | Default HTTP port when --port is not given |
AGENTNET_CONFIG_DIR | Default config directory |
AGENTNET_LOG_LEVEL | debug / info / warn / error |
BEAGLE_ENS_GATEWAY | Gateway 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,wsandyargs.
Version, source and changelog
- Verified version: 0.1.132 (published 2026-11-06; both quickstarts were re-run against it after 0.1.131 was first documented, with the same results and identical type declarations).
- npm: https://www.npmjs.com/package/@decentnetwork/beagle
- Source repository is not public yet. Browse the published code at https://www.npmjs.com/package/@decentnetwork/beagle?activeTab=code
- License: MIT.
- Changelog: none is published in the package.