@vdoninja/ninja-p2p 0.1.0

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/dashboard.html ADDED
@@ -0,0 +1,602 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>NinjaClawd P2P Dashboard</title>
7
+ <style>
8
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
9
+ :root{
10
+ --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;
11
+ --fg:#e6edf3;--fg2:#8b949e;--accent:#58a6ff;--green:#3fb950;
12
+ --red:#f85149;--yellow:#d29922;--purple:#bc8cff;
13
+ --font:ui-monospace,SFMono-Regular,'SF Mono',Menlo,Consolas,monospace;
14
+ }
15
+ body{background:var(--bg);color:var(--fg);font-family:var(--font);font-size:13px;line-height:1.5;display:flex;height:100vh;overflow:hidden}
16
+ a{color:var(--accent);text-decoration:none}
17
+ button{font-family:var(--font);cursor:pointer;font-size:12px;border:1px solid var(--border);background:var(--bg3);color:var(--fg);border-radius:6px;padding:4px 12px;transition:background .15s}
18
+ button:hover{background:var(--border)}
19
+ button.primary{background:var(--accent);color:#000;border-color:var(--accent)}
20
+ button.primary:hover{opacity:.85}
21
+ input,select{font-family:var(--font);font-size:12px;background:var(--bg);color:var(--fg);border:1px solid var(--border);border-radius:6px;padding:5px 8px;outline:none}
22
+ input:focus,select:focus{border-color:var(--accent)}
23
+
24
+ /* Layout */
25
+ #sidebar{width:240px;background:var(--bg2);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0}
26
+ #sidebar h1{font-size:14px;padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px}
27
+ #sidebar h1 .dot{width:8px;height:8px;border-radius:50%;background:var(--red);flex-shrink:0}
28
+ #sidebar h1 .dot.on{background:var(--green)}
29
+ #main{flex:1;display:flex;flex-direction:column;overflow:hidden}
30
+
31
+ /* Connection Panel */
32
+ #conn-panel{padding:12px 16px;border-bottom:1px solid var(--border);background:var(--bg2)}
33
+ #conn-panel label{display:block;font-size:11px;color:var(--fg2);margin-bottom:2px;margin-top:8px}
34
+ #conn-panel label:first-child{margin-top:0}
35
+ #conn-panel input{width:100%;margin-bottom:2px}
36
+ #conn-panel .row{display:flex;gap:6px;margin-top:10px}
37
+ #conn-panel .row button{flex:1}
38
+ #conn-panel .status{font-size:11px;color:var(--fg2);margin-top:6px;text-align:center}
39
+
40
+ /* Peer List */
41
+ #peer-section{flex:1;overflow-y:auto;padding:8px 0}
42
+ #peer-section h2{font-size:11px;color:var(--fg2);padding:4px 16px;text-transform:uppercase;letter-spacing:.5px}
43
+ .peer-item{padding:6px 16px;cursor:pointer;display:flex;align-items:center;gap:8px;transition:background .1s}
44
+ .peer-item:hover{background:var(--bg3)}
45
+ .peer-item.selected{background:var(--bg3);border-left:2px solid var(--accent)}
46
+ .peer-item .indicator{width:6px;height:6px;border-radius:50%;flex-shrink:0}
47
+ .peer-item .indicator.online{background:var(--green)}
48
+ .peer-item .indicator.offline{background:var(--fg2)}
49
+ .peer-item .info{min-width:0}
50
+ .peer-item .name{font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
51
+ .peer-item .role{font-size:10px;color:var(--fg2)}
52
+
53
+ /* Main content */
54
+ #messages{flex:1;overflow-y:auto;padding:12px 16px;display:flex;flex-direction:column;gap:2px}
55
+ .msg{padding:2px 0;word-break:break-word}
56
+ .msg .ts{color:var(--fg2);font-size:11px;margin-right:6px}
57
+ .msg .sender{font-weight:600;margin-right:4px}
58
+ .msg.type-chat .sender{color:var(--accent)}
59
+ .msg.type-announce .sender{color:var(--green)}
60
+ .msg.type-event .sender{color:var(--purple)}
61
+ .msg.type-command .sender{color:var(--yellow)}
62
+ .msg.type-command_response .sender{color:var(--yellow)}
63
+ .msg.type-system{color:var(--fg2);font-style:italic}
64
+ .msg.type-system .sender{color:var(--fg2)}
65
+ .msg .body{color:var(--fg)}
66
+ .msg .meta{color:var(--fg2);font-size:11px}
67
+
68
+ /* Chat Input */
69
+ #chat-bar{padding:8px 16px;border-top:1px solid var(--border);background:var(--bg2);display:flex;gap:6px;align-items:center}
70
+ #chat-bar input{flex:1}
71
+ #chat-bar select{width:auto;min-width:100px}
72
+
73
+ /* Skills panel */
74
+ #skills-panel{padding:8px 16px;border-top:1px solid var(--border);font-size:11px;color:var(--fg2);max-height:80px;overflow-y:auto}
75
+ .skill-tag{display:inline-block;background:var(--bg3);border:1px solid var(--border);border-radius:4px;padding:1px 6px;margin:1px 2px;font-size:10px}
76
+
77
+ /* Responsive */
78
+ @media(max-width:600px){
79
+ #sidebar{width:100%;height:auto;max-height:40vh;border-right:none;border-bottom:1px solid var(--border)}
80
+ body{flex-direction:column}
81
+ }
82
+ </style>
83
+ </head>
84
+ <body>
85
+
86
+ <div id="sidebar">
87
+ <h1><span class="dot" id="conn-dot"></span> NinjaClawd</h1>
88
+
89
+ <div id="conn-panel">
90
+ <label>Room</label>
91
+ <input id="inp-room" placeholder="clawd_..." autocomplete="off">
92
+ <label>Password</label>
93
+ <input id="inp-password" placeholder="false = no encryption" autocomplete="off">
94
+ <label>Your Name</label>
95
+ <input id="inp-name" value="Dashboard" autocomplete="off">
96
+ <div class="row">
97
+ <button class="primary" id="btn-connect">Connect</button>
98
+ <button id="btn-disconnect" disabled>Disconnect</button>
99
+ </div>
100
+ <div class="status" id="conn-status">Disconnected</div>
101
+ </div>
102
+
103
+ <div id="peer-section">
104
+ <h2>Peers (<span id="peer-count">0</span>)</h2>
105
+ <div id="peer-list"></div>
106
+ </div>
107
+
108
+ <div id="skills-panel"></div>
109
+ </div>
110
+
111
+ <div id="main">
112
+ <div id="messages"></div>
113
+ <div id="chat-bar">
114
+ <select id="chat-target">
115
+ <option value="">Broadcast</option>
116
+ </select>
117
+ <input id="chat-input" placeholder="Type a message... (/help for commands)" autocomplete="off">
118
+ <button class="primary" id="btn-send">Send</button>
119
+ </div>
120
+ </div>
121
+
122
+ <script src="https://cdn.jsdelivr.net/gh/steveseguin/ninjasdk@latest/vdoninja-sdk.min.js"></script>
123
+ <script>
124
+ (function() {
125
+ 'use strict';
126
+
127
+ // ── State ──────────────────────────────────────────────────────────────
128
+ let sdk = null;
129
+ let connected = false;
130
+ let myStreamId = '';
131
+ let myIdentity = null;
132
+ const peers = new Map(); // streamId -> {uuid, identity, skills, status, connected}
133
+ const messages = [];
134
+ const MAX_MESSAGES = 500;
135
+ let selectedPeer = ''; // empty = broadcast
136
+
137
+ // ── DOM refs ───────────────────────────────────────────────────────────
138
+ const $ = (id) => document.getElementById(id);
139
+ const dot = $('conn-dot');
140
+ const statusEl = $('conn-status');
141
+ const peerCountEl = $('peer-count');
142
+ const peerListEl = $('peer-list');
143
+ const messagesEl = $('messages');
144
+ const chatInput = $('chat-input');
145
+ const chatTarget = $('chat-target');
146
+ const skillsPanel = $('skills-panel');
147
+ const btnConnect = $('btn-connect');
148
+ const btnDisconnect = $('btn-disconnect');
149
+ const btnSend = $('btn-send');
150
+ const inpRoom = $('inp-room');
151
+ const inpPassword = $('inp-password');
152
+ const inpName = $('inp-name');
153
+
154
+ // ── URL params ─────────────────────────────────────────────────────────
155
+ const params = new URLSearchParams(window.location.search);
156
+ if (params.get('room')) inpRoom.value = params.get('room');
157
+ if (params.get('password')) inpPassword.value = params.get('password');
158
+ if (params.get('name')) inpName.value = params.get('name');
159
+
160
+ // Restore from localStorage
161
+ if (!inpRoom.value) inpRoom.value = localStorage.getItem('clawd_room') || '';
162
+ if (!inpPassword.value) inpPassword.value = localStorage.getItem('clawd_password') || 'false';
163
+ if (!inpName.value || inpName.value === 'Dashboard') inpName.value = localStorage.getItem('clawd_name') || 'Dashboard';
164
+
165
+ // ── Helpers ────────────────────────────────────────────────────────────
166
+ function randomId() {
167
+ const arr = new Uint8Array(8);
168
+ crypto.getRandomValues(arr);
169
+ return Array.from(arr, b => b.toString(16).padStart(2, '0')).join('');
170
+ }
171
+
172
+ function instanceId() {
173
+ return randomId().slice(0, 8);
174
+ }
175
+
176
+ function fmtTime(ts) {
177
+ const d = new Date(ts);
178
+ return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
179
+ }
180
+
181
+ function escHtml(s) {
182
+ const d = document.createElement('div');
183
+ d.textContent = s;
184
+ return d.innerHTML;
185
+ }
186
+
187
+ // ── Message Display ────────────────────────────────────────────────────
188
+ function addMessage(type, sender, body, ts, meta) {
189
+ const msg = { type, sender, body, ts: ts || Date.now(), meta };
190
+ messages.push(msg);
191
+ if (messages.length > MAX_MESSAGES) messages.shift();
192
+ renderMessage(msg);
193
+ }
194
+
195
+ function renderMessage(msg) {
196
+ const div = document.createElement('div');
197
+ div.className = 'msg type-' + msg.type;
198
+ let html = '<span class="ts">' + fmtTime(msg.ts) + '</span>';
199
+ html += '<span class="sender">' + escHtml(msg.sender) + '</span> ';
200
+ html += '<span class="body">' + escHtml(String(msg.body)) + '</span>';
201
+ if (msg.meta) html += ' <span class="meta">' + escHtml(msg.meta) + '</span>';
202
+ div.innerHTML = html;
203
+ messagesEl.appendChild(div);
204
+ messagesEl.scrollTop = messagesEl.scrollHeight;
205
+ }
206
+
207
+ // ── Peer List ──────────────────────────────────────────────────────────
208
+ function renderPeers() {
209
+ peerListEl.innerHTML = '';
210
+ peerCountEl.textContent = peers.size;
211
+ // Update chat target dropdown
212
+ const curVal = chatTarget.value;
213
+ chatTarget.innerHTML = '<option value="">Broadcast</option>';
214
+
215
+ for (const [sid, p] of peers) {
216
+ // Peer list item
217
+ const div = document.createElement('div');
218
+ div.className = 'peer-item' + (sid === selectedPeer ? ' selected' : '');
219
+ const name = p.identity?.name || sid;
220
+ const role = p.identity?.role || 'unknown';
221
+ div.innerHTML = `
222
+ <div class="indicator ${p.connected ? 'online' : 'offline'}"></div>
223
+ <div class="info">
224
+ <div class="name">${escHtml(name)}</div>
225
+ <div class="role">${escHtml(role)}${p.status && p.status !== 'online' ? ' - ' + escHtml(p.status) : ''}</div>
226
+ </div>`;
227
+ div.addEventListener('click', () => {
228
+ selectedPeer = selectedPeer === sid ? '' : sid;
229
+ chatTarget.value = selectedPeer;
230
+ renderPeers();
231
+ renderSkills();
232
+ });
233
+ peerListEl.appendChild(div);
234
+
235
+ // Chat target option
236
+ const opt = document.createElement('option');
237
+ opt.value = sid;
238
+ opt.textContent = name;
239
+ chatTarget.appendChild(opt);
240
+ }
241
+ chatTarget.value = curVal || '';
242
+ }
243
+
244
+ function renderSkills() {
245
+ if (!selectedPeer) {
246
+ skillsPanel.innerHTML = '';
247
+ return;
248
+ }
249
+ const p = peers.get(selectedPeer);
250
+ if (!p || !p.skills || p.skills.length === 0) {
251
+ skillsPanel.innerHTML = '<em>No skills announced</em>';
252
+ return;
253
+ }
254
+ skillsPanel.innerHTML = '<strong>Skills:</strong> ' +
255
+ p.skills.map(s => '<span class="skill-tag">' + escHtml(s) + '</span>').join('');
256
+ }
257
+
258
+ // ── Envelope Protocol ──────────────────────────────────────────────────
259
+ function createEnvelope(type, payload, opts) {
260
+ return {
261
+ v: 1,
262
+ id: randomId(),
263
+ type: type,
264
+ from: myIdentity,
265
+ to: (opts && opts.to) || null,
266
+ topic: (opts && opts.topic) || null,
267
+ ts: Date.now(),
268
+ payload: payload
269
+ };
270
+ }
271
+
272
+ function parseEnvelope(data) {
273
+ try {
274
+ const obj = typeof data === 'string' ? JSON.parse(data) : data;
275
+ if (obj && obj.v === 1 && typeof obj.type === 'string' && obj.from) return obj;
276
+ return null;
277
+ } catch(e) { return null; }
278
+ }
279
+
280
+ // ── Connection ─────────────────────────────────────────────────────────
281
+ async function doConnect() {
282
+ const room = inpRoom.value.trim();
283
+ if (!room) { alert('Room name is required'); return; }
284
+
285
+ const pw = inpPassword.value.trim();
286
+ const password = pw === 'false' ? false : (pw || undefined);
287
+ const name = inpName.value.trim() || 'Dashboard';
288
+ myStreamId = 'dashboard_' + randomId().slice(0, 8);
289
+ myIdentity = {
290
+ streamId: myStreamId,
291
+ role: 'dashboard',
292
+ name: name,
293
+ instanceId: instanceId()
294
+ };
295
+
296
+ // Save to localStorage
297
+ localStorage.setItem('clawd_room', room);
298
+ localStorage.setItem('clawd_password', inpPassword.value.trim());
299
+ localStorage.setItem('clawd_name', name);
300
+
301
+ statusEl.textContent = 'Connecting...';
302
+ btnConnect.disabled = true;
303
+
304
+ try {
305
+ sdk = new VDONinjaSDK({
306
+ host: 'wss://wss.vdo.ninja',
307
+ debug: false
308
+ });
309
+
310
+ // Wire events
311
+ sdk.addEventListener('peerConnected', (e) => {
312
+ const uuid = e.detail?.uuid || 'unknown';
313
+ const sid = e.detail?.streamID || uuid;
314
+ if (!peers.has(sid)) {
315
+ peers.set(sid, { uuid, identity: null, skills: [], status: 'online', connected: true });
316
+ } else {
317
+ const p = peers.get(sid);
318
+ p.uuid = uuid;
319
+ p.connected = true;
320
+ }
321
+ renderPeers();
322
+ });
323
+
324
+ sdk.addEventListener('dataChannelOpen', (e) => {
325
+ const uuid = e.detail?.uuid;
326
+ // Send our announce
327
+ const announce = createEnvelope('announce', {
328
+ skills: ['dashboard-ui', 'chat'],
329
+ status: 'online',
330
+ version: '0.1.0',
331
+ topics: ['events', 'bots', 'support']
332
+ });
333
+ try { sdk.sendData(announce, { UUID: uuid }); } catch(err) {}
334
+ });
335
+
336
+ sdk.addEventListener('dataReceived', (e) => {
337
+ const raw = e.detail?.data;
338
+ const uuid = e.detail?.uuid || '';
339
+ const env = parseEnvelope(raw);
340
+ if (!env) return;
341
+
342
+ const sender = env.from.streamId;
343
+ // Update peer info
344
+ if (!peers.has(sender)) {
345
+ peers.set(sender, { uuid, identity: env.from, skills: [], status: 'online', connected: true });
346
+ }
347
+
348
+ switch (env.type) {
349
+ case 'announce': {
350
+ const p = peers.get(sender);
351
+ if (p) {
352
+ p.identity = env.from;
353
+ p.skills = env.payload?.skills || [];
354
+ p.status = env.payload?.status || 'online';
355
+ }
356
+ addMessage('announce', env.from.name || sender,
357
+ `joined as ${env.from.role || '?'} with skills: ${(env.payload?.skills || []).join(', ')}`,
358
+ env.ts);
359
+ renderPeers();
360
+ renderSkills();
361
+ break;
362
+ }
363
+ case 'skill_update': {
364
+ const p = peers.get(sender);
365
+ if (p) {
366
+ p.skills = env.payload?.skills || p.skills;
367
+ p.status = env.payload?.status || p.status;
368
+ }
369
+ addMessage('event', env.from.name || sender,
370
+ `status: ${env.payload?.status || '?'}${env.payload?.statusDetail ? ' - ' + env.payload.statusDetail : ''}`,
371
+ env.ts);
372
+ renderPeers();
373
+ renderSkills();
374
+ break;
375
+ }
376
+ case 'chat':
377
+ addMessage('chat', env.from.name || sender,
378
+ env.payload?.text || env.payload?.message || JSON.stringify(env.payload),
379
+ env.ts,
380
+ env.topic ? '[' + env.topic + ']' : undefined);
381
+ break;
382
+ case 'event':
383
+ addMessage('event', env.from.name || sender,
384
+ env.payload?.kind ? env.payload.kind + ': ' + JSON.stringify(env.payload) : JSON.stringify(env.payload),
385
+ env.ts,
386
+ env.topic ? '[' + env.topic + ']' : undefined);
387
+ break;
388
+ case 'command':
389
+ addMessage('command', env.from.name || sender,
390
+ 'CMD: ' + (env.payload?.command || '?') + ' ' + JSON.stringify(env.payload?.args || {}),
391
+ env.ts);
392
+ break;
393
+ case 'command_response':
394
+ addMessage('command_response', env.from.name || sender,
395
+ JSON.stringify(env.payload, null, 2),
396
+ env.ts);
397
+ break;
398
+ case 'ping':
399
+ // Auto-respond with pong
400
+ try {
401
+ const pong = createEnvelope('pong', { pingId: env.id, pongTs: Date.now() }, { to: sender });
402
+ sdk.sendData(pong, { UUID: uuid });
403
+ } catch(err) {}
404
+ break;
405
+ case 'history_replay': {
406
+ // Replay contains an original message as payload
407
+ const orig = env.payload;
408
+ if (orig && orig.type) {
409
+ addMessage(orig.type, (orig.from?.name || 'replay'),
410
+ orig.payload?.text || orig.payload?.message || JSON.stringify(orig.payload),
411
+ orig.ts,
412
+ '[replay]');
413
+ }
414
+ break;
415
+ }
416
+ default:
417
+ addMessage(env.type, env.from.name || sender,
418
+ JSON.stringify(env.payload),
419
+ env.ts);
420
+ }
421
+ });
422
+
423
+ sdk.addEventListener('peerDisconnected', (e) => {
424
+ const uuid = e.detail?.uuid || '';
425
+ const sid = e.detail?.streamID || '';
426
+ // Find by uuid if no streamID
427
+ let key = sid;
428
+ if (!key) {
429
+ for (const [k, v] of peers) { if (v.uuid === uuid) { key = k; break; } }
430
+ }
431
+ if (key && peers.has(key)) {
432
+ const p = peers.get(key);
433
+ const name = p.identity?.name || key;
434
+ p.connected = false;
435
+ addMessage('system', name, 'disconnected', Date.now());
436
+ renderPeers();
437
+ }
438
+ });
439
+
440
+ sdk.addEventListener('listing', (e) => {
441
+ const list = e.detail?.list || [];
442
+ for (const entry of list) {
443
+ if (entry.streamID && entry.streamID !== myStreamId) {
444
+ sdk.view(entry.streamID, { audio: false, video: false });
445
+ }
446
+ }
447
+ });
448
+
449
+ sdk.addEventListener('disconnected', () => {
450
+ statusEl.textContent = 'WebSocket lost, reconnecting...';
451
+ dot.classList.remove('on');
452
+ });
453
+
454
+ sdk.addEventListener('reconnected', () => {
455
+ statusEl.textContent = 'Reconnected';
456
+ dot.classList.add('on');
457
+ });
458
+
459
+ // Connect
460
+ await sdk.connect();
461
+ const joinOpts = { room: room };
462
+ if (password !== undefined) joinOpts.password = password;
463
+ await sdk.joinRoom(joinOpts);
464
+ await sdk.announce({ streamID: myStreamId });
465
+
466
+ connected = true;
467
+ dot.classList.add('on');
468
+ statusEl.textContent = 'Connected as ' + myStreamId;
469
+ btnConnect.disabled = true;
470
+ btnDisconnect.disabled = false;
471
+ inpRoom.disabled = true;
472
+ inpPassword.disabled = true;
473
+
474
+ addMessage('system', 'System', 'Connected to room: ' + room, Date.now());
475
+
476
+ // Request history from any peers
477
+ setTimeout(() => {
478
+ if (sdk && connected) {
479
+ const req = createEnvelope('history_request', { count: 50 });
480
+ try { sdk.sendData(req); } catch(err) {}
481
+ }
482
+ }, 2000);
483
+
484
+ } catch(err) {
485
+ statusEl.textContent = 'Error: ' + err.message;
486
+ btnConnect.disabled = false;
487
+ console.error('Connection error:', err);
488
+ }
489
+ }
490
+
491
+ function doDisconnect() {
492
+ if (sdk) {
493
+ try { sdk.leaveRoom(); } catch(e) {}
494
+ try { sdk.disconnect(); } catch(e) {}
495
+ }
496
+ sdk = null;
497
+ connected = false;
498
+ peers.clear();
499
+ dot.classList.remove('on');
500
+ statusEl.textContent = 'Disconnected';
501
+ btnConnect.disabled = false;
502
+ btnDisconnect.disabled = true;
503
+ inpRoom.disabled = false;
504
+ inpPassword.disabled = false;
505
+ renderPeers();
506
+ addMessage('system', 'System', 'Disconnected', Date.now());
507
+ }
508
+
509
+ // ── Chat Commands ──────────────────────────────────────────────────────
510
+ function handleInput() {
511
+ const text = chatInput.value.trim();
512
+ if (!text) return;
513
+ chatInput.value = '';
514
+
515
+ // Local commands
516
+ if (text.startsWith('/')) {
517
+ const parts = text.slice(1).split(' ');
518
+ const cmd = parts[0].toLowerCase();
519
+ switch (cmd) {
520
+ case 'help':
521
+ addMessage('system', 'System',
522
+ 'Commands: /help, /peers, /status, /history [n], /health, /clear, /dm <peer> <msg>',
523
+ Date.now());
524
+ return;
525
+ case 'peers':
526
+ if (!connected) { addMessage('system', 'System', 'Not connected', Date.now()); return; }
527
+ // Send command to all bots
528
+ sendEnvelope('command', { command: 'peers' });
529
+ return;
530
+ case 'status':
531
+ if (!connected) { addMessage('system', 'System', 'Not connected', Date.now()); return; }
532
+ sendEnvelope('command', { command: 'status' });
533
+ return;
534
+ case 'history':
535
+ if (!connected) { addMessage('system', 'System', 'Not connected', Date.now()); return; }
536
+ sendEnvelope('command', { command: 'history', args: { count: parseInt(parts[1]) || 50 } });
537
+ return;
538
+ case 'health':
539
+ if (!connected) { addMessage('system', 'System', 'Not connected', Date.now()); return; }
540
+ sendEnvelope('command', { command: 'health' });
541
+ return;
542
+ case 'clear':
543
+ messagesEl.innerHTML = '';
544
+ messages.length = 0;
545
+ return;
546
+ case 'dm':
547
+ if (parts.length < 3) { addMessage('system', 'System', 'Usage: /dm <peer_stream_id> <message>', Date.now()); return; }
548
+ const target = parts[1];
549
+ const msg = parts.slice(2).join(' ');
550
+ sendEnvelope('chat', { text: msg }, { to: target });
551
+ addMessage('chat', myIdentity.name + ' -> ' + target, msg, Date.now());
552
+ return;
553
+ default:
554
+ addMessage('system', 'System', 'Unknown command: /' + cmd + '. Try /help', Date.now());
555
+ return;
556
+ }
557
+ }
558
+
559
+ // Regular chat message
560
+ if (!connected) { addMessage('system', 'System', 'Not connected', Date.now()); return; }
561
+ const target = chatTarget.value || null;
562
+ sendEnvelope('chat', { text: text }, target ? { to: target } : undefined);
563
+ addMessage('chat', myIdentity.name + (target ? ' -> ' + target : ''), text, Date.now());
564
+ }
565
+
566
+ function sendEnvelope(type, payload, opts) {
567
+ if (!sdk || !connected) return;
568
+ const env = createEnvelope(type, payload, opts);
569
+ try {
570
+ if (opts && opts.to) {
571
+ // Find the peer's UUID for targeted send
572
+ const p = peers.get(opts.to);
573
+ if (p) {
574
+ sdk.sendData(env, { UUID: p.uuid });
575
+ } else {
576
+ sdk.sendData(env, { streamID: opts.to });
577
+ }
578
+ } else {
579
+ sdk.sendData(env);
580
+ }
581
+ } catch(err) {
582
+ addMessage('system', 'System', 'Send error: ' + err.message, Date.now());
583
+ }
584
+ }
585
+
586
+ // ── Event Bindings ─────────────────────────────────────────────────────
587
+ btnConnect.addEventListener('click', doConnect);
588
+ btnDisconnect.addEventListener('click', doDisconnect);
589
+ btnSend.addEventListener('click', handleInput);
590
+ chatInput.addEventListener('keydown', (e) => {
591
+ if (e.key === 'Enter') { e.preventDefault(); handleInput(); }
592
+ });
593
+ chatTarget.addEventListener('change', () => { selectedPeer = chatTarget.value; renderPeers(); });
594
+
595
+ // Auto-connect if room is provided via URL params
596
+ if (params.get('room') && params.get('autoconnect') !== 'false') {
597
+ setTimeout(doConnect, 500);
598
+ }
599
+ })();
600
+ </script>
601
+ </body>
602
+ </html>
@@ -0,0 +1,4 @@
1
+ export { MessageBus, type KeywordTrigger, type MessageBusOptions, type SendDataFn } from "./message-bus.js";
2
+ export { PeerRegistry, type PeerRecord, type PeerRegistryEvents } from "./peer-registry.js";
3
+ export { createEnvelope, createInstanceId, createMessageId, envelopeToWire, generateRoomName, isValidEnvelope, parseEnvelope, type AnnouncePayload, type MessageEnvelope, type MessageType, type PeerIdentity, type SkillUpdatePayload, } from "./protocol.js";
4
+ export { VDOBridge, type VDOBridgeOptions } from "./vdo-bridge.js";
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { MessageBus } from "./message-bus.js";
2
+ export { PeerRegistry } from "./peer-registry.js";
3
+ export { createEnvelope, createInstanceId, createMessageId, envelopeToWire, generateRoomName, isValidEnvelope, parseEnvelope, } from "./protocol.js";
4
+ export { VDOBridge } from "./vdo-bridge.js";