@vdoninja/ninja-p2p 0.1.1 → 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 MessageType, 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,6 +65,10 @@ 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;
61
72
  /** Send raw data through the underlying SDK without envelope wrapping. */
62
73
  sendRaw(data: unknown, targetStreamId?: string): boolean;
63
74
  /** Reply to a received message using its sender as the target. */
@@ -75,4 +86,5 @@ export declare class VDOBridge extends EventEmitter {
75
86
  private stopHeartbeat;
76
87
  private respondPong;
77
88
  private broadcastSkillUpdate;
89
+ private maybeViewPeer;
78
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.1";
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,26 +167,34 @@ 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
+ }
152
178
  /** Send raw data through the underlying SDK without envelope wrapping. */
153
179
  sendRaw(data, targetStreamId) {
154
180
  if (!this.sdk)
155
181
  return false;
156
182
  try {
157
183
  if (!targetStreamId) {
158
- this.sdk.sendData(data);
184
+ this.sdk.sendData(data, { allowFallback: true });
159
185
  return true;
160
186
  }
161
187
  const peer = this.peers.getPeer(targetStreamId);
162
188
  if (peer?.uuid) {
163
- this.sdk.sendData(data, { UUID: peer.uuid });
189
+ this.sdk.sendData(data, { UUID: peer.uuid, allowFallback: true });
164
190
  }
165
191
  else {
166
- this.sdk.sendData(data, { streamID: targetStreamId });
192
+ this.sdk.sendData(data, { streamID: targetStreamId, allowFallback: true });
167
193
  }
168
194
  return true;
169
195
  }
170
196
  catch (err) {
171
- this.emit("error", err);
197
+ this.emitBridgeError(err);
172
198
  return false;
173
199
  }
174
200
  }
@@ -297,6 +323,7 @@ export class VDOBridge extends EventEmitter {
297
323
  this.sdk.addEventListener("peerDisconnected", (event) => {
298
324
  const uuid = event.detail?.uuid ?? "unknown";
299
325
  const streamId = event.detail?.streamID ?? this.peers.streamIdForUuid(uuid) ?? uuid;
326
+ this.viewedStreamIds.delete(streamId);
300
327
  this.peers.markDisconnected(streamId);
301
328
  console.log(`[P2P] Peer disconnected: ${streamId}`);
302
329
  this.emit("peer:disconnected", { streamId, uuid });
@@ -315,15 +342,20 @@ export class VDOBridge extends EventEmitter {
315
342
  const list = event.detail?.list ?? [];
316
343
  for (const entry of list) {
317
344
  if (entry.streamID && entry.streamID !== this.options.streamId) {
318
- // View each existing peer to establish data channels
319
- this.sdk.view(entry.streamID, { audio: false, video: false });
345
+ this.maybeViewPeer(entry.streamID);
320
346
  }
321
347
  }
322
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
+ });
323
355
  // Error handling
324
356
  this.sdk.addEventListener("error", (event) => {
325
357
  console.error("[P2P] SDK error:", event.detail?.error);
326
- this.emit("error", event.detail?.error);
358
+ this.emitBridgeError(event.detail?.error);
327
359
  });
328
360
  }
329
361
  // ── Heartbeat ────────────────────────────────────────────────────────────
@@ -371,6 +403,20 @@ export class VDOBridge extends EventEmitter {
371
403
  skills: this.skills,
372
404
  status: this.status,
373
405
  statusDetail: this.statusDetail,
406
+ agent: this.agentProfile,
374
407
  });
375
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
+ }
376
422
  }
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@vdoninja/ninja-p2p",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
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,13 +48,17 @@
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
64
  "keywords": [
@@ -48,6 +67,10 @@
48
67
  "vdo.ninja",
49
68
  "agent",
50
69
  "agent-mesh",
70
+ "cli",
71
+ "codex",
72
+ "claude-code",
73
+ "skill",
51
74
  "pub-sub",
52
75
  "data-channel",
53
76
  "bot",
@@ -1,70 +0,0 @@
1
- ---
2
- name: ninja-p2p
3
- description: Use VDO.Ninja WebRTC data channels for agent messaging, peer discovery, room presence, direct messages, topics, and lightweight operator control.
4
- ---
5
-
6
- # ninja-p2p
7
-
8
- Use this when a project needs agents to meet in a room and talk to each other over WebRTC.
9
-
10
- ## What It Gives You
11
-
12
- - `VDOBridge` for connection lifecycle and SDK wiring
13
- - `PeerRegistry` for presence, skills, topics, and status
14
- - `MessageBus` for room chat, direct messages, topic fanout, history replay, and the in-memory offline queue
15
- - `dashboard.html` for a browser room monitor/chat client
16
-
17
- ## Install
18
-
19
- ```bash
20
- npm install @vdoninja/ninja-p2p @roamhq/wrtc
21
- ```
22
-
23
- `@vdoninja/sdk` is installed automatically. `ws` comes from the SDK in Node.
24
-
25
- ## Default Pattern
26
-
27
- 1. Pick a shared `room`.
28
- 2. Give each peer a unique `streamId`.
29
- 3. Connect with `VDOBridge`.
30
- 4. Use `bridge.chat()` for room messages.
31
- 5. Use `bridge.chat(..., targetStreamId)` or `bridge.command()` for direct messages.
32
- 6. Use `bridge.publishEvent()` for topic messages.
33
- 7. Use `bridge.commandResponse()`, `bridge.ack()`, or `bridge.reply()` when a peer should answer back.
34
-
35
- ## Example
36
-
37
- ```ts
38
- import { VDOBridge } from "@vdoninja/ninja-p2p";
39
-
40
- const bridge = new VDOBridge({
41
- room: "agents_room",
42
- streamId: "planner_bot",
43
- identity: {
44
- streamId: "planner_bot",
45
- role: "agent",
46
- name: "Planner",
47
- },
48
- password: false,
49
- skills: ["plan", "chat"],
50
- topics: ["events"],
51
- });
52
-
53
- await bridge.connect();
54
-
55
- bridge.chat("Planner online");
56
- bridge.chat("sync now", "worker_bot");
57
- bridge.publishEvent("events", "status_change", { status: "busy" });
58
-
59
- bridge.bus.on("message:chat", (envelope) => {
60
- console.log(`${envelope.from.name}: ${envelope.payload.text}`);
61
- });
62
- ```
63
-
64
- ## Notes
65
-
66
- - `room` and `streamId` should be stable and human-readable.
67
- - Private messages target peer `streamId`s.
68
- - Topic messages are broadcast and filtered on the receiver side.
69
- - The offline queue is in memory, not durable storage.
70
- - `sendRaw()` and `getSDK()` are there if you need lower-level SDK access for binary data or media work.