@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.
- package/.agents/skills/ninja-p2p/SKILL.md +142 -0
- package/.agents/skills/ninja-p2p/agents/openai.yaml +5 -0
- package/.claude/skills/ninja-p2p/SKILL.md +130 -0
- package/.codex/skills/ninja-p2p/SKILL.md +142 -0
- package/.codex/skills/ninja-p2p/agents/openai.yaml +5 -0
- package/README.md +628 -53
- package/dashboard.html +595 -56
- package/dist/agent-state.d.ts +167 -0
- package/dist/agent-state.js +304 -0
- package/dist/cli-lib.d.ts +132 -0
- package/dist/cli-lib.js +602 -0
- package/dist/cli.d.ts +25 -0
- package/dist/cli.js +1337 -0
- package/dist/file-transfer.d.ts +54 -0
- package/dist/file-transfer.js +241 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -0
- package/dist/message-bus.d.ts +1 -0
- package/dist/message-bus.js +10 -3
- package/dist/peer-registry.d.ts +2 -1
- package/dist/peer-registry.js +4 -0
- package/dist/protocol.d.ts +57 -1
- package/dist/shared-folders.d.ts +31 -0
- package/dist/shared-folders.js +137 -0
- package/dist/vdo-bridge.d.ts +13 -1
- package/dist/vdo-bridge.js +55 -9
- package/package.json +25 -2
- package/skills/ninja-p2p/SKILL.md +0 -70
package/dist/vdo-bridge.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/vdo-bridge.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|