@vdoninja/ninja-p2p 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,7 +10,7 @@
10
10
  import { EventEmitter } from "node:events";
11
11
  import { MessageBus, type MessageBusOptions } from "./message-bus.js";
12
12
  import { PeerRegistry } from "./peer-registry.js";
13
- import { type AnnouncePayload, type MessageEnvelope, type PeerIdentity } from "./protocol.js";
13
+ import { type AgentProfile, type FileOfferPayload, type AnnouncePayload, type MessageEnvelope, type MessageType, type PeerIdentity } from "./protocol.js";
14
14
  export type VDOBridgeOptions = {
15
15
  room: string;
16
16
  streamId: string;
@@ -23,6 +23,8 @@ export type VDOBridgeOptions = {
23
23
  skills?: string[];
24
24
  /** Topics to subscribe to. */
25
25
  topics?: string[];
26
+ /** Optional agent profile advertised to peers. */
27
+ agentProfile?: AgentProfile;
26
28
  /** Heartbeat interval in ms. Default: 30000. */
27
29
  heartbeatMs?: number;
28
30
  /** MessageBus options. */
@@ -40,14 +42,19 @@ export declare class VDOBridge extends EventEmitter {
40
42
  private status;
41
43
  private statusDetail;
42
44
  private version;
45
+ private agentProfile;
46
+ private readonly viewedStreamIds;
43
47
  constructor(options: VDOBridgeOptions);
44
48
  connect(): Promise<void>;
45
49
  disconnect(): Promise<void>;
46
50
  isConnected(): boolean;
51
+ private emitBridgeError;
47
52
  /** Update skills and broadcast the change. */
48
53
  updateSkills(skills: string[]): void;
49
54
  /** Update status and broadcast the change. */
50
55
  updateStatus(status: string, detail?: string): void;
56
+ /** Update the advertised agent profile and broadcast the change. */
57
+ updateAgentProfile(profile: AgentProfile): void;
51
58
  /** Get current announce payload. */
52
59
  getAnnouncePayload(): AnnouncePayload;
53
60
  /** Send a chat message to everyone or a specific peer. */
@@ -58,9 +65,26 @@ export declare class VDOBridge extends EventEmitter {
58
65
  command(targetStreamId: string, command: string, args?: unknown): MessageEnvelope;
59
66
  /** Publish an event to a topic. */
60
67
  publishEvent(topic: string, kind: string, data?: unknown): MessageEnvelope;
68
+ /** Send a file to a connected peer. */
69
+ sendFile(targetStreamId: string, filePath: string): FileOfferPayload;
70
+ /** Send an image to a connected peer. */
71
+ sendImage(targetStreamId: string, filePath: string): FileOfferPayload;
72
+ /** Send raw data through the underlying SDK without envelope wrapping. */
73
+ sendRaw(data: unknown, targetStreamId?: string): boolean;
74
+ /** Reply to a received message using its sender as the target. */
75
+ reply(message: MessageEnvelope, type: MessageType, payload: unknown): MessageEnvelope;
76
+ /** Acknowledge receipt of a received message. */
77
+ ack(message: MessageEnvelope, payload?: unknown): MessageEnvelope;
78
+ /** Respond to a command or request-style message. */
79
+ commandResponse(message: MessageEnvelope, result?: unknown, error?: string): MessageEnvelope;
80
+ /** Ask a peer to replay recent message history to this bridge. */
81
+ requestHistory(targetStreamId: string, count?: number): MessageEnvelope;
82
+ /** Access the underlying VDO.Ninja SDK instance for advanced media workflows. */
83
+ getSDK(): InstanceType<typeof import("@vdoninja/sdk")> | null;
61
84
  private wireSDKEvents;
62
85
  private startHeartbeat;
63
86
  private stopHeartbeat;
64
87
  private respondPong;
65
88
  private broadcastSkillUpdate;
89
+ private maybeViewPeer;
66
90
  }
@@ -8,6 +8,7 @@
8
8
  * Zero dependencies on stevesbot internals.
9
9
  */
10
10
  import { EventEmitter } from "node:events";
11
+ import { sendFileFromPath } from "./file-transfer.js";
11
12
  import { MessageBus } from "./message-bus.js";
12
13
  import { PeerRegistry } from "./peer-registry.js";
13
14
  import { createEnvelope, createInstanceId, envelopeToWire, parseEnvelope, } from "./protocol.js";
@@ -23,7 +24,9 @@ export class VDOBridge extends EventEmitter {
23
24
  skills;
24
25
  status = "idle";
25
26
  statusDetail = "";
26
- version = "0.1.0";
27
+ version = "0.1.2";
28
+ agentProfile;
29
+ viewedStreamIds = new Set();
27
30
  constructor(options) {
28
31
  super();
29
32
  this.options = options;
@@ -34,6 +37,7 @@ export class VDOBridge extends EventEmitter {
34
37
  instanceId: createInstanceId(),
35
38
  };
36
39
  this.skills = options.skills ?? [];
40
+ this.agentProfile = options.agentProfile;
37
41
  this.peers = new PeerRegistry();
38
42
  this.bus = new MessageBus(this.identity, this.peers, options.busOptions);
39
43
  // Subscribe to requested topics
@@ -63,7 +67,7 @@ export class VDOBridge extends EventEmitter {
63
67
  this.sdk.sendData(data, target ?? undefined);
64
68
  }
65
69
  catch (err) {
66
- this.emit("error", err);
70
+ this.emitBridgeError(err);
67
71
  }
68
72
  });
69
73
  await this.sdk.connect();
@@ -100,6 +104,7 @@ export class VDOBridge extends EventEmitter {
100
104
  catch { /* best effort */ }
101
105
  this.connected = false;
102
106
  this.sdk = null;
107
+ this.viewedStreamIds.clear();
103
108
  this.peers.clear();
104
109
  console.log("[P2P] Disconnected.");
105
110
  this.emit("disconnected");
@@ -107,6 +112,13 @@ export class VDOBridge extends EventEmitter {
107
112
  isConnected() {
108
113
  return this.connected;
109
114
  }
115
+ emitBridgeError(err) {
116
+ if (this.listenerCount("error") > 0) {
117
+ this.emit("error", err);
118
+ return;
119
+ }
120
+ console.error("[P2P] Bridge error:", err);
121
+ }
110
122
  // ── Identity Management ──────────────────────────────────────────────────
111
123
  /** Update skills and broadcast the change. */
112
124
  updateSkills(skills) {
@@ -119,6 +131,11 @@ export class VDOBridge extends EventEmitter {
119
131
  this.statusDetail = detail ?? "";
120
132
  this.broadcastSkillUpdate();
121
133
  }
134
+ /** Update the advertised agent profile and broadcast the change. */
135
+ updateAgentProfile(profile) {
136
+ this.agentProfile = profile;
137
+ this.broadcastSkillUpdate();
138
+ }
122
139
  /** Get current announce payload. */
123
140
  getAnnouncePayload() {
124
141
  return {
@@ -127,6 +144,7 @@ export class VDOBridge extends EventEmitter {
127
144
  statusDetail: this.statusDetail,
128
145
  version: this.version,
129
146
  topics: this.bus.getSubscriptions(),
147
+ agent: this.agentProfile,
130
148
  };
131
149
  }
132
150
  // ── Convenience Methods ──────────────────────────────────────────────────
@@ -149,6 +167,72 @@ export class VDOBridge extends EventEmitter {
149
167
  publishEvent(topic, kind, data) {
150
168
  return this.bus.publish(topic, "event", { kind, ...((data && typeof data === "object") ? data : { data }) });
151
169
  }
170
+ /** Send a file to a connected peer. */
171
+ sendFile(targetStreamId, filePath) {
172
+ return sendFileFromPath(this, targetStreamId, filePath, "file");
173
+ }
174
+ /** Send an image to a connected peer. */
175
+ sendImage(targetStreamId, filePath) {
176
+ return sendFileFromPath(this, targetStreamId, filePath, "image");
177
+ }
178
+ /** Send raw data through the underlying SDK without envelope wrapping. */
179
+ sendRaw(data, targetStreamId) {
180
+ if (!this.sdk)
181
+ return false;
182
+ try {
183
+ if (!targetStreamId) {
184
+ this.sdk.sendData(data, { allowFallback: true });
185
+ return true;
186
+ }
187
+ const peer = this.peers.getPeer(targetStreamId);
188
+ if (peer?.uuid) {
189
+ this.sdk.sendData(data, { UUID: peer.uuid, allowFallback: true });
190
+ }
191
+ else {
192
+ this.sdk.sendData(data, { streamID: targetStreamId, allowFallback: true });
193
+ }
194
+ return true;
195
+ }
196
+ catch (err) {
197
+ this.emitBridgeError(err);
198
+ return false;
199
+ }
200
+ }
201
+ /** Reply to a received message using its sender as the target. */
202
+ reply(message, type, payload) {
203
+ return this.bus.send(message.from.streamId, type, payload);
204
+ }
205
+ /** Acknowledge receipt of a received message. */
206
+ ack(message, payload) {
207
+ const ackPayload = { messageId: message.id };
208
+ if (payload !== undefined) {
209
+ ackPayload.data = payload;
210
+ }
211
+ return this.bus.send(message.from.streamId, "ack", ackPayload);
212
+ }
213
+ /** Respond to a command or request-style message. */
214
+ commandResponse(message, result, error) {
215
+ if (error) {
216
+ return this.bus.send(message.from.streamId, "command_response", {
217
+ requestId: message.id,
218
+ ok: false,
219
+ error,
220
+ });
221
+ }
222
+ return this.bus.send(message.from.streamId, "command_response", {
223
+ requestId: message.id,
224
+ ok: true,
225
+ result: result ?? null,
226
+ });
227
+ }
228
+ /** Ask a peer to replay recent message history to this bridge. */
229
+ requestHistory(targetStreamId, count = 50) {
230
+ return this.bus.send(targetStreamId, "history_request", { count });
231
+ }
232
+ /** Access the underlying VDO.Ninja SDK instance for advanced media workflows. */
233
+ getSDK() {
234
+ return this.sdk;
235
+ }
152
236
  // ── SDK Event Wiring ─────────────────────────────────────────────────────
153
237
  wireSDKEvents() {
154
238
  if (!this.sdk)
@@ -239,6 +323,7 @@ export class VDOBridge extends EventEmitter {
239
323
  this.sdk.addEventListener("peerDisconnected", (event) => {
240
324
  const uuid = event.detail?.uuid ?? "unknown";
241
325
  const streamId = event.detail?.streamID ?? this.peers.streamIdForUuid(uuid) ?? uuid;
326
+ this.viewedStreamIds.delete(streamId);
242
327
  this.peers.markDisconnected(streamId);
243
328
  console.log(`[P2P] Peer disconnected: ${streamId}`);
244
329
  this.emit("peer:disconnected", { streamId, uuid });
@@ -257,15 +342,20 @@ export class VDOBridge extends EventEmitter {
257
342
  const list = event.detail?.list ?? [];
258
343
  for (const entry of list) {
259
344
  if (entry.streamID && entry.streamID !== this.options.streamId) {
260
- // View each existing peer to establish data channels
261
- this.sdk.view(entry.streamID, { audio: false, video: false });
345
+ this.maybeViewPeer(entry.streamID);
262
346
  }
263
347
  }
264
348
  });
349
+ this.sdk.addEventListener("videoaddedtoroom", (event) => {
350
+ this.maybeViewPeer(event.detail?.streamID);
351
+ });
352
+ this.sdk.addEventListener("streamAdded", (event) => {
353
+ this.maybeViewPeer(event.detail?.streamID);
354
+ });
265
355
  // Error handling
266
356
  this.sdk.addEventListener("error", (event) => {
267
357
  console.error("[P2P] SDK error:", event.detail?.error);
268
- this.emit("error", event.detail?.error);
358
+ this.emitBridgeError(event.detail?.error);
269
359
  });
270
360
  }
271
361
  // ── Heartbeat ────────────────────────────────────────────────────────────
@@ -313,6 +403,20 @@ export class VDOBridge extends EventEmitter {
313
403
  skills: this.skills,
314
404
  status: this.status,
315
405
  statusDetail: this.statusDetail,
406
+ agent: this.agentProfile,
316
407
  });
317
408
  }
409
+ maybeViewPeer(streamId) {
410
+ if (!this.sdk || !streamId || streamId === this.options.streamId || this.viewedStreamIds.has(streamId)) {
411
+ return;
412
+ }
413
+ this.viewedStreamIds.add(streamId);
414
+ try {
415
+ this.sdk.view(streamId, { audio: false, video: false });
416
+ }
417
+ catch (err) {
418
+ this.viewedStreamIds.delete(streamId);
419
+ this.emitBridgeError(err);
420
+ }
421
+ }
318
422
  }
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@vdoninja/ninja-p2p",
3
- "version": "0.1.0",
4
- "description": "Reusable P2P agent communication module using VDO.Ninja WebRTC data channels",
3
+ "version": "0.1.2",
4
+ "description": "Open source P2P agent messaging and coordination over VDO.Ninja WebRTC data channels",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
8
+ "bin": {
9
+ "ninja-p2p": "./dist/cli.js"
10
+ },
8
11
  "exports": {
9
12
  ".": {
10
13
  "types": "./dist/index.d.ts",
@@ -22,6 +25,18 @@
22
25
  "types": "./dist/peer-registry.d.ts",
23
26
  "import": "./dist/peer-registry.js"
24
27
  },
28
+ "./file-transfer": {
29
+ "types": "./dist/file-transfer.d.ts",
30
+ "import": "./dist/file-transfer.js"
31
+ },
32
+ "./agent-state": {
33
+ "types": "./dist/agent-state.d.ts",
34
+ "import": "./dist/agent-state.js"
35
+ },
36
+ "./shared-folders": {
37
+ "types": "./dist/shared-folders.d.ts",
38
+ "import": "./dist/shared-folders.js"
39
+ },
25
40
  "./protocol": {
26
41
  "types": "./dist/protocol.d.ts",
27
42
  "import": "./dist/protocol.js"
@@ -33,16 +48,36 @@
33
48
  "dist",
34
49
  "dashboard.html",
35
50
  "README.md",
36
- "skills/ninja-p2p"
51
+ ".codex/skills/ninja-p2p",
52
+ ".agents/skills/ninja-p2p",
53
+ ".claude/skills/ninja-p2p"
37
54
  ],
38
55
  "sideEffects": false,
39
56
  "scripts": {
40
57
  "test": "tsx --test tests/**/*.test.ts",
41
58
  "prebuild": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
42
59
  "build": "tsc -p tsconfig.json",
60
+ "validate:live": "node ./scripts/validate-live.mjs",
61
+ "sync:docs": "node -e \"const fs=require('node:fs'); fs.mkdirSync('docs',{recursive:true}); fs.copyFileSync('dashboard.html','docs/index.html');\"",
43
62
  "prepare": "npm run build"
44
63
  },
45
- "keywords": ["p2p", "webrtc", "vdo.ninja", "agent", "pub-sub", "data-channel", "bot"],
64
+ "keywords": [
65
+ "p2p",
66
+ "webrtc",
67
+ "vdo.ninja",
68
+ "agent",
69
+ "agent-mesh",
70
+ "cli",
71
+ "codex",
72
+ "claude-code",
73
+ "skill",
74
+ "pub-sub",
75
+ "data-channel",
76
+ "bot",
77
+ "nat-traversal",
78
+ "hole-punching",
79
+ "remote-control"
80
+ ],
46
81
  "author": "Steve Seguin",
47
82
  "license": "MIT",
48
83
  "publishConfig": {
@@ -1,74 +0,0 @@
1
- ---
2
- name: ninja-p2p
3
- description: Build bot-to-bot communication over VDO.Ninja WebRTC data channels. Use this skill when the user wants agents or services to meet in a room, discover peers, send private messages, broadcast events, or keep lightweight P2P presence without a central relay.
4
- ---
5
-
6
- # ninja-p2p
7
-
8
- Use this skill when a project needs room-based peer discovery and direct messaging between bots over WebRTC.
9
-
10
- ## What This Repo Provides
11
-
12
- - `VDOBridge` for connection lifecycle, room join, announce, heartbeat, and SDK wiring
13
- - `PeerRegistry` for presence, skills, topics, and status tracking
14
- - `MessageBus` for broadcast, direct messages, topic-based fanout, history replay, and offline queueing
15
- - `dashboard.html` for a browser-side room monitor and chat UI
16
-
17
- ## Install The Library In A Project
18
-
19
- Install it from npm:
20
-
21
- ```bash
22
- npm install @vdoninja/ninja-p2p @roamhq/wrtc
23
- ```
24
-
25
- `@vdoninja/sdk` is installed transitively. `ws` comes from the SDK in Node environments.
26
-
27
- ## Add This Skill To Codex
28
-
29
- Use Codex's built-in `skill-installer` helper and install the `skills/ninja-p2p` path from this repo, then restart Codex.
30
-
31
- ## Default Integration Pattern
32
-
33
- 1. Pick a shared `room` for the bots that should discover each other.
34
- 2. Give each bot a unique `streamId`.
35
- 3. Connect with `VDOBridge`.
36
- 4. Use `bridge.chat(text)` for room-wide messages.
37
- 5. Use `bridge.chat(text, targetStreamId)` or `bridge.command(targetStreamId, command, args)` for private messages.
38
- 6. Use `bridge.publishEvent(topic, kind, data)` for topic fanout.
39
-
40
- ## Example
41
-
42
- ```ts
43
- import { VDOBridge } from "@vdoninja/ninja-p2p";
44
-
45
- const bridge = new VDOBridge({
46
- room: "agents_room",
47
- streamId: "planner_bot",
48
- identity: {
49
- streamId: "planner_bot",
50
- role: "agent",
51
- name: "Planner",
52
- },
53
- password: false,
54
- skills: ["plan", "chat"],
55
- topics: ["events"],
56
- });
57
-
58
- await bridge.connect();
59
-
60
- bridge.chat("Planner online");
61
- bridge.chat("sync now", "worker_bot");
62
- bridge.publishEvent("events", "status_change", { status: "busy" });
63
-
64
- bridge.bus.on("message:chat", (envelope) => {
65
- console.log(`${envelope.from.name}: ${envelope.payload.text}`);
66
- });
67
- ```
68
-
69
- ## Notes
70
-
71
- - `room` and `streamId` should be stable and human-readable, but unique enough to avoid collisions.
72
- - Private messages target peer `streamId`s.
73
- - Topic messages are broadcast and filtered on the receiver side.
74
- - The browser dashboard can sit in the same room as the bots for live inspection.