@vdoninja/ninja-p2p 0.1.1 → 0.1.4
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 +580 -55
- package/dashboard.html +595 -56
- package/dist/agent-state.d.ts +167 -0
- package/dist/agent-state.js +311 -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 +3 -1
- package/dist/message-bus.js +33 -16
- 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 +59 -13
- package/docs/images/agent-room-dashboard.png +0 -0
- package/docs/protocol-and-reliability.md +38 -0
- package/package.json +29 -3
- package/skills/ninja-p2p/SKILL.md +0 -70
package/dashboard.html
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<title>
|
|
6
|
+
<title>Ninja P2P Dashboard</title>
|
|
7
7
|
<style>
|
|
8
8
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
9
9
|
:root{
|
|
@@ -22,11 +22,11 @@ input,select{font-family:var(--font);font-size:12px;background:var(--bg);color:v
|
|
|
22
22
|
input:focus,select:focus{border-color:var(--accent)}
|
|
23
23
|
|
|
24
24
|
/* Layout */
|
|
25
|
-
#sidebar{width:240px;background:var(--bg2);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0}
|
|
25
|
+
#sidebar{width:240px;min-width:0;background:var(--bg2);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0}
|
|
26
26
|
#sidebar h1{font-size:14px;padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px}
|
|
27
27
|
#sidebar h1 .dot{width:8px;height:8px;border-radius:50%;background:var(--red);flex-shrink:0}
|
|
28
28
|
#sidebar h1 .dot.on{background:var(--green)}
|
|
29
|
-
#main{flex:1;display:flex;flex-direction:column;overflow:hidden}
|
|
29
|
+
#main{flex:1;min-width:0;display:flex;flex-direction:column;overflow:hidden}
|
|
30
30
|
|
|
31
31
|
/* Connection Panel */
|
|
32
32
|
#conn-panel{padding:12px 16px;border-bottom:1px solid var(--border);background:var(--bg2)}
|
|
@@ -34,7 +34,7 @@ input:focus,select:focus{border-color:var(--accent)}
|
|
|
34
34
|
#conn-panel label:first-child{margin-top:0}
|
|
35
35
|
#conn-panel input{width:100%;margin-bottom:2px}
|
|
36
36
|
#conn-panel .row{display:flex;gap:6px;margin-top:10px}
|
|
37
|
-
#conn-panel .row button{flex:1}
|
|
37
|
+
#conn-panel .row button{flex:1;min-width:0}
|
|
38
38
|
#conn-panel .status{font-size:11px;color:var(--fg2);margin-top:6px;text-align:center}
|
|
39
39
|
|
|
40
40
|
/* Peer List */
|
|
@@ -51,7 +51,7 @@ input:focus,select:focus{border-color:var(--accent)}
|
|
|
51
51
|
.peer-item .role{font-size:10px;color:var(--fg2)}
|
|
52
52
|
|
|
53
53
|
/* Main content */
|
|
54
|
-
#messages{flex:1;overflow-y:auto;padding:12px 16px;display:flex;flex-direction:column;gap:2px}
|
|
54
|
+
#messages{flex:1;min-width:0;overflow-y:auto;padding:12px 16px;display:flex;flex-direction:column;gap:2px}
|
|
55
55
|
.msg{padding:2px 0;word-break:break-word}
|
|
56
56
|
.msg .ts{color:var(--fg2);font-size:11px;margin-right:6px}
|
|
57
57
|
.msg .sender{font-weight:600;margin-right:4px}
|
|
@@ -62,29 +62,62 @@ input:focus,select:focus{border-color:var(--accent)}
|
|
|
62
62
|
.msg.type-command_response .sender{color:var(--yellow)}
|
|
63
63
|
.msg.type-system{color:var(--fg2);font-style:italic}
|
|
64
64
|
.msg.type-system .sender{color:var(--fg2)}
|
|
65
|
-
.msg .body{color:var(--fg)}
|
|
66
|
-
.msg .meta{color:var(--fg2);font-size:11px}
|
|
65
|
+
.msg .body{color:var(--fg);overflow-wrap:anywhere}
|
|
66
|
+
.msg .meta{color:var(--fg2);font-size:11px;overflow-wrap:anywhere}
|
|
67
67
|
|
|
68
68
|
/* Chat Input */
|
|
69
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}
|
|
70
|
+
#chat-bar input{flex:1;min-width:0}
|
|
71
|
+
#chat-bar select{width:auto;min-width:100px;max-width:100%}
|
|
72
72
|
|
|
73
|
-
/*
|
|
74
|
-
#
|
|
73
|
+
/* Peer details panel */
|
|
74
|
+
#details-panel{padding:10px 16px;border-top:1px solid var(--border);font-size:11px;color:var(--fg2);max-height:280px;min-width:0;overflow-y:auto;overflow-x:hidden}
|
|
75
|
+
.details-title{font-size:11px;color:var(--fg2);text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px}
|
|
76
|
+
.details-block{margin-bottom:8px}
|
|
77
|
+
.details-label{color:var(--fg2);display:block;margin-bottom:2px}
|
|
78
|
+
.details-value{color:var(--fg);word-break:break-word;overflow-wrap:anywhere}
|
|
75
79
|
.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}
|
|
80
|
+
.share-tag{display:inline-flex;max-width:100%;align-items:center;gap:4px;flex-wrap:wrap;background:var(--bg3);border:1px solid var(--border);border-radius:4px;padding:1px 6px;margin:1px 2px;font-size:10px}
|
|
81
|
+
.share-tag button{padding:1px 6px;font-size:10px}
|
|
82
|
+
.ask-item{margin:3px 0;padding-left:10px;position:relative}
|
|
83
|
+
.ask-item::before{content:'>';position:absolute;left:0;color:var(--fg2)}
|
|
84
|
+
.details-actions,.share-browser-actions{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px}
|
|
85
|
+
.details-actions button,.share-browser-actions button{padding:3px 8px;flex:1 1 calc(50% - 3px);min-width:0}
|
|
86
|
+
.share-browser{border:1px solid var(--border);border-radius:6px;padding:8px;background:var(--bg)}
|
|
87
|
+
.share-browser .row{display:flex;gap:6px;align-items:center;margin-top:6px}
|
|
88
|
+
.share-browser .row:first-child{margin-top:0}
|
|
89
|
+
.share-browser .row input,.share-browser .row select{flex:1;min-width:0}
|
|
90
|
+
.share-list{margin-top:8px;border-top:1px solid var(--border);padding-top:6px}
|
|
91
|
+
.share-entry{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;padding:4px 0}
|
|
92
|
+
.share-entry .path{flex:1;color:var(--fg);word-break:break-word}
|
|
93
|
+
.share-entry .meta{color:var(--fg2);font-size:10px;white-space:nowrap}
|
|
94
|
+
.share-empty{color:var(--fg2)}
|
|
76
95
|
|
|
77
96
|
/* Responsive */
|
|
78
|
-
@media(max-width:
|
|
79
|
-
|
|
80
|
-
|
|
97
|
+
@media(max-width:960px){
|
|
98
|
+
body{flex-direction:column;height:auto;min-height:100vh;overflow:auto}
|
|
99
|
+
#sidebar{width:100%;height:auto;max-height:none;border-right:none;border-bottom:1px solid var(--border)}
|
|
100
|
+
#peer-section{flex:none;max-height:180px}
|
|
101
|
+
#details-panel{max-height:220px}
|
|
102
|
+
#chat-bar{flex-wrap:wrap;align-items:stretch}
|
|
103
|
+
#chat-bar select{flex:1 1 160px;min-width:0}
|
|
104
|
+
#chat-bar input{flex:1 1 100%}
|
|
105
|
+
#chat-bar button{flex:0 0 auto}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@media(max-width:700px){
|
|
109
|
+
#conn-panel .row,
|
|
110
|
+
.share-browser .row{flex-direction:column;align-items:stretch}
|
|
111
|
+
.details-actions button,
|
|
112
|
+
.share-browser-actions button,
|
|
113
|
+
#chat-bar button{flex:1 1 100%}
|
|
81
114
|
}
|
|
82
115
|
</style>
|
|
83
116
|
</head>
|
|
84
117
|
<body>
|
|
85
118
|
|
|
86
119
|
<div id="sidebar">
|
|
87
|
-
<h1><span class="dot" id="conn-dot"></span>
|
|
120
|
+
<h1><span class="dot" id="conn-dot"></span> Ninja P2P</h1>
|
|
88
121
|
|
|
89
122
|
<div id="conn-panel">
|
|
90
123
|
<label>Room</label>
|
|
@@ -105,7 +138,7 @@ input:focus,select:focus{border-color:var(--accent)}
|
|
|
105
138
|
<div id="peer-list"></div>
|
|
106
139
|
</div>
|
|
107
140
|
|
|
108
|
-
<div id="
|
|
141
|
+
<div id="details-panel"></div>
|
|
109
142
|
</div>
|
|
110
143
|
|
|
111
144
|
<div id="main">
|
|
@@ -129,10 +162,15 @@ input:focus,select:focus{border-color:var(--accent)}
|
|
|
129
162
|
let connected = false;
|
|
130
163
|
let myStreamId = '';
|
|
131
164
|
let myIdentity = null;
|
|
165
|
+
const DASHBOARD_VERSION = '0.1.4';
|
|
132
166
|
const peers = new Map(); // streamId -> {uuid, identity, skills, status, connected}
|
|
167
|
+
const peerUuidToStream = new Map();
|
|
133
168
|
const messages = [];
|
|
134
169
|
const MAX_MESSAGES = 500;
|
|
135
170
|
let selectedPeer = ''; // empty = broadcast
|
|
171
|
+
const pendingRequests = new Map(); // requestId -> {command,target,args}
|
|
172
|
+
const incomingTransfers = new Map(); // transferId -> browser-side receive state
|
|
173
|
+
const shareBrowser = { peer: '', share: '', path: '', entries: [], truncated: false };
|
|
136
174
|
|
|
137
175
|
// ── DOM refs ───────────────────────────────────────────────────────────
|
|
138
176
|
const $ = (id) => document.getElementById(id);
|
|
@@ -143,7 +181,7 @@ input:focus,select:focus{border-color:var(--accent)}
|
|
|
143
181
|
const messagesEl = $('messages');
|
|
144
182
|
const chatInput = $('chat-input');
|
|
145
183
|
const chatTarget = $('chat-target');
|
|
146
|
-
const
|
|
184
|
+
const detailsPanel = $('details-panel');
|
|
147
185
|
const btnConnect = $('btn-connect');
|
|
148
186
|
const btnDisconnect = $('btn-disconnect');
|
|
149
187
|
const btnSend = $('btn-send');
|
|
@@ -158,9 +196,9 @@ input:focus,select:focus{border-color:var(--accent)}
|
|
|
158
196
|
if (params.get('name')) inpName.value = params.get('name');
|
|
159
197
|
|
|
160
198
|
// Restore from localStorage
|
|
161
|
-
if (!inpRoom.value) inpRoom.value = localStorage.getItem('
|
|
162
|
-
if (!inpPassword.value) inpPassword.value = localStorage.getItem('
|
|
163
|
-
if (!inpName.value || inpName.value === 'Dashboard') inpName.value = localStorage.getItem('
|
|
199
|
+
if (!inpRoom.value) inpRoom.value = localStorage.getItem('ninja_room') || '';
|
|
200
|
+
if (!inpPassword.value) inpPassword.value = localStorage.getItem('ninja_password') || 'false';
|
|
201
|
+
if (!inpName.value || inpName.value === 'Dashboard') inpName.value = localStorage.getItem('ninja_name') || 'Dashboard';
|
|
164
202
|
|
|
165
203
|
// ── Helpers ────────────────────────────────────────────────────────────
|
|
166
204
|
function randomId() {
|
|
@@ -184,9 +222,83 @@ input:focus,select:focus{border-color:var(--accent)}
|
|
|
184
222
|
return d.innerHTML;
|
|
185
223
|
}
|
|
186
224
|
|
|
225
|
+
function bytesFromBase64(value) {
|
|
226
|
+
const binary = atob(value);
|
|
227
|
+
const bytes = new Uint8Array(binary.length);
|
|
228
|
+
for (let i = 0; i < binary.length; i += 1) {
|
|
229
|
+
bytes[i] = binary.charCodeAt(i);
|
|
230
|
+
}
|
|
231
|
+
return bytes;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function concatBytes(parts, totalSize) {
|
|
235
|
+
const output = new Uint8Array(totalSize);
|
|
236
|
+
let offset = 0;
|
|
237
|
+
for (const part of parts) {
|
|
238
|
+
output.set(part, offset);
|
|
239
|
+
offset += part.length;
|
|
240
|
+
}
|
|
241
|
+
return output;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function sha256Hex(bytes) {
|
|
245
|
+
const digest = await crypto.subtle.digest('SHA-256', bytes);
|
|
246
|
+
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, '0')).join('');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function fmtBytes(size) {
|
|
250
|
+
if (typeof size !== 'number' || !Number.isFinite(size)) return '';
|
|
251
|
+
if (size < 1024) return size + ' B';
|
|
252
|
+
if (size < 1024 * 1024) return (size / 1024).toFixed(1) + ' KB';
|
|
253
|
+
return (size / (1024 * 1024)).toFixed(1) + ' MB';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function getSelectedPeerRecord() {
|
|
257
|
+
return selectedPeer ? peers.get(selectedPeer) : null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function getPeerKeyByUuid(uuid) {
|
|
261
|
+
if (!uuid) return '';
|
|
262
|
+
return peerUuidToStream.get(uuid) || '';
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function upsertPeer(streamId, uuid, identity) {
|
|
266
|
+
const key = streamId || uuid;
|
|
267
|
+
if (!key) return null;
|
|
268
|
+
|
|
269
|
+
const knownKey = uuid ? getPeerKeyByUuid(uuid) : '';
|
|
270
|
+
if (uuid && streamId && knownKey && knownKey !== streamId && peers.has(knownKey)) {
|
|
271
|
+
const existing = peers.get(knownKey);
|
|
272
|
+
peers.delete(knownKey);
|
|
273
|
+
peers.set(streamId, Object.assign(existing || {}, { uuid, identity: identity || existing?.identity || null }));
|
|
274
|
+
peerUuidToStream.set(uuid, streamId);
|
|
275
|
+
return peers.get(streamId);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const provisional = uuid && uuid !== key && peers.has(uuid) ? peers.get(uuid) : null;
|
|
279
|
+
if (provisional && !peers.has(key)) {
|
|
280
|
+
peers.delete(uuid);
|
|
281
|
+
peers.set(key, Object.assign(provisional, { uuid, identity: identity || provisional.identity || null }));
|
|
282
|
+
peerUuidToStream.set(uuid, key);
|
|
283
|
+
return peers.get(key);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (!peers.has(key)) {
|
|
287
|
+
peers.set(key, { uuid, identity: identity || null, skills: [], status: 'online', connected: true });
|
|
288
|
+
} else {
|
|
289
|
+
const peer = peers.get(key);
|
|
290
|
+
peer.uuid = uuid || peer.uuid || '';
|
|
291
|
+
peer.identity = identity || peer.identity || null;
|
|
292
|
+
peer.connected = true;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (uuid) peerUuidToStream.set(uuid, key);
|
|
296
|
+
return peers.get(key);
|
|
297
|
+
}
|
|
298
|
+
|
|
187
299
|
// ── Message Display ────────────────────────────────────────────────────
|
|
188
|
-
function addMessage(type, sender, body, ts, meta) {
|
|
189
|
-
const msg = { type, sender, body, ts: ts || Date.now(), meta };
|
|
300
|
+
function addMessage(type, sender, body, ts, meta, extra) {
|
|
301
|
+
const msg = Object.assign({ type, sender, body, ts: ts || Date.now(), meta }, extra || {});
|
|
190
302
|
messages.push(msg);
|
|
191
303
|
if (messages.length > MAX_MESSAGES) messages.shift();
|
|
192
304
|
renderMessage(msg);
|
|
@@ -200,6 +312,14 @@ input:focus,select:focus{border-color:var(--accent)}
|
|
|
200
312
|
html += '<span class="body">' + escHtml(String(msg.body)) + '</span>';
|
|
201
313
|
if (msg.meta) html += ' <span class="meta">' + escHtml(msg.meta) + '</span>';
|
|
202
314
|
div.innerHTML = html;
|
|
315
|
+
if (msg.downloadUrl) {
|
|
316
|
+
const link = document.createElement('a');
|
|
317
|
+
link.href = msg.downloadUrl;
|
|
318
|
+
link.download = msg.downloadName || 'download';
|
|
319
|
+
link.textContent = ' download';
|
|
320
|
+
link.className = 'meta';
|
|
321
|
+
div.appendChild(link);
|
|
322
|
+
}
|
|
203
323
|
messagesEl.appendChild(div);
|
|
204
324
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
205
325
|
}
|
|
@@ -228,7 +348,7 @@ input:focus,select:focus{border-color:var(--accent)}
|
|
|
228
348
|
selectedPeer = selectedPeer === sid ? '' : sid;
|
|
229
349
|
chatTarget.value = selectedPeer;
|
|
230
350
|
renderPeers();
|
|
231
|
-
|
|
351
|
+
renderDetails();
|
|
232
352
|
});
|
|
233
353
|
peerListEl.appendChild(div);
|
|
234
354
|
|
|
@@ -241,18 +361,180 @@ input:focus,select:focus{border-color:var(--accent)}
|
|
|
241
361
|
chatTarget.value = curVal || '';
|
|
242
362
|
}
|
|
243
363
|
|
|
244
|
-
function
|
|
364
|
+
function renderDetails() {
|
|
245
365
|
if (!selectedPeer) {
|
|
246
|
-
|
|
366
|
+
detailsPanel.innerHTML = '<div class="details-title">Peer Details</div><div class="details-value">Select a bot to see its profile, capabilities, shares, and quick actions.</div>';
|
|
247
367
|
return;
|
|
248
368
|
}
|
|
249
369
|
const p = peers.get(selectedPeer);
|
|
250
|
-
if (!p
|
|
251
|
-
|
|
370
|
+
if (!p) {
|
|
371
|
+
detailsPanel.innerHTML = '<div class="details-title">Peer Details</div><div class="details-value">Peer not found.</div>';
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const agent = p.agentProfile || {};
|
|
375
|
+
const skills = (p.skills || []).map((s) => '<span class="skill-tag">' + escHtml(s) + '</span>').join(' ');
|
|
376
|
+
const can = (agent.can || []).map((s) => '<span class="skill-tag">' + escHtml(s) + '</span>').join(' ');
|
|
377
|
+
const asks = (agent.asks || []).map((ask) =>
|
|
378
|
+
'<div class="ask-item"><strong>' + escHtml(ask.name) + '</strong>: ' + escHtml(ask.description || '') + '</div>'
|
|
379
|
+
).join('');
|
|
380
|
+
const shares = Array.isArray(agent.shares) ? agent.shares : [];
|
|
381
|
+
|
|
382
|
+
if (shareBrowser.peer !== selectedPeer) {
|
|
383
|
+
shareBrowser.peer = selectedPeer;
|
|
384
|
+
shareBrowser.share = shares[0] && shares[0].name ? shares[0].name : '';
|
|
385
|
+
shareBrowser.path = '';
|
|
386
|
+
shareBrowser.entries = [];
|
|
387
|
+
shareBrowser.truncated = false;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const shareOptions = shares.map((share) =>
|
|
391
|
+
'<option value="' + escHtml(share.name) + '"' + (shareBrowser.share === share.name ? ' selected' : '') + '>' + escHtml(share.name) + '</option>'
|
|
392
|
+
).join('');
|
|
393
|
+
const shareTags = shares.map((share) =>
|
|
394
|
+
'<span class="share-tag">' +
|
|
395
|
+
escHtml(share.name) +
|
|
396
|
+
'<button type="button" data-share-open-root="' + escHtml(share.name) + '">Browse</button>' +
|
|
397
|
+
'</span>'
|
|
398
|
+
).join(' ');
|
|
399
|
+
const shareEntries = (shareBrowser.entries || []).map((entry) =>
|
|
400
|
+
'<div class="share-entry">' +
|
|
401
|
+
'<div class="path">' + escHtml(entry.path) + '</div>' +
|
|
402
|
+
'<div class="meta">' + escHtml(entry.type === 'dir' ? 'folder' : fmtBytes(entry.size)) + '</div>' +
|
|
403
|
+
(entry.type === 'dir'
|
|
404
|
+
? '<button type="button" data-share-open="' + escHtml(entry.path) + '">Open</button>'
|
|
405
|
+
: '<button type="button" data-share-get="' + escHtml(entry.path) + '">Get</button>') +
|
|
406
|
+
'</div>'
|
|
407
|
+
).join('');
|
|
408
|
+
|
|
409
|
+
detailsPanel.innerHTML = [
|
|
410
|
+
'<div class="details-title">Peer Details</div>',
|
|
411
|
+
'<div class="details-block"><span class="details-label">Name</span><div class="details-value">' + escHtml(p.identity?.name || selectedPeer) + '</div></div>',
|
|
412
|
+
'<div class="details-block"><span class="details-label">Role</span><div class="details-value">' + escHtml(p.identity?.role || 'unknown') + '</div></div>',
|
|
413
|
+
'<div class="details-block"><span class="details-label">Status</span><div class="details-value">' + escHtml(p.status || 'online') + (p.statusDetail ? ' - ' + escHtml(p.statusDetail) : '') + '</div></div>',
|
|
414
|
+
'<div class="details-actions">' +
|
|
415
|
+
'<button type="button" data-peer-command="status">Status</button>' +
|
|
416
|
+
'<button type="button" data-peer-command="profile">Profile</button>' +
|
|
417
|
+
'<button type="button" data-peer-command="capabilities">Capabilities</button>' +
|
|
418
|
+
'<button type="button" data-peer-command="shares">Shares</button>' +
|
|
419
|
+
'</div>',
|
|
420
|
+
agent.summary ? '<div class="details-block"><span class="details-label">Summary</span><div class="details-value">' + escHtml(agent.summary) + '</div></div>' : '',
|
|
421
|
+
(agent.runtime || agent.provider || agent.model)
|
|
422
|
+
? '<div class="details-block"><span class="details-label">Runtime</span><div class="details-value">' + escHtml([agent.runtime, agent.provider, agent.model].filter(Boolean).join(' / ')) + '</div></div>'
|
|
423
|
+
: '',
|
|
424
|
+
agent.workspace ? '<div class="details-block"><span class="details-label">Workspace</span><div class="details-value">' + escHtml(agent.workspace) + '</div></div>' : '',
|
|
425
|
+
skills ? '<div class="details-block"><span class="details-label">Skills</span><div class="details-value">' + skills + '</div></div>' : '',
|
|
426
|
+
can ? '<div class="details-block"><span class="details-label">Can</span><div class="details-value">' + can + '</div></div>' : '',
|
|
427
|
+
asks ? '<div class="details-block"><span class="details-label">Asks</span><div class="details-value">' + asks + '</div></div>' : '',
|
|
428
|
+
shareTags ? '<div class="details-block"><span class="details-label">Shared Folders</span><div class="details-value">' + shareTags + '</div></div>' : '',
|
|
429
|
+
shares.length ? '<div class="details-block"><span class="details-label">Browse Shared Files</span><div class="share-browser">' +
|
|
430
|
+
'<div class="row"><select id="share-select">' + shareOptions + '</select><input id="share-path" placeholder="folder or file path" value="' + escHtml(shareBrowser.path || '') + '" autocomplete="off"></div>' +
|
|
431
|
+
'<div class="share-browser-actions"><button type="button" id="btn-share-list">List</button><button type="button" id="btn-share-get">Get File</button></div>' +
|
|
432
|
+
'<div class="share-list">' +
|
|
433
|
+
(shareBrowser.entries.length
|
|
434
|
+
? shareEntries + (shareBrowser.truncated ? '<div class="share-empty">Showing the first entries only.</div>' : '')
|
|
435
|
+
: '<div class="share-empty">' + escHtml(shareBrowser.share ? 'No listing loaded yet. Click Browse or List.' : 'Select a shared folder first.') + '</div>') +
|
|
436
|
+
'</div>' +
|
|
437
|
+
'</div></div>' : '',
|
|
438
|
+
p.topics?.length ? '<div class="details-block"><span class="details-label">Topics</span><div class="details-value">' + escHtml(p.topics.join(', ')) + '</div></div>' : '',
|
|
439
|
+
p.version ? '<div class="details-block"><span class="details-label">Version</span><div class="details-value">' + escHtml(p.version) + '</div></div>' : ''
|
|
440
|
+
].join('');
|
|
441
|
+
|
|
442
|
+
detailsPanel.querySelectorAll('[data-peer-command]').forEach((button) => {
|
|
443
|
+
button.addEventListener('click', () => {
|
|
444
|
+
sendPeerCommand(selectedPeer, button.getAttribute('data-peer-command'));
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
detailsPanel.querySelectorAll('[data-share-open-root]').forEach((button) => {
|
|
449
|
+
button.addEventListener('click', () => {
|
|
450
|
+
const share = button.getAttribute('data-share-open-root') || '';
|
|
451
|
+
shareBrowser.peer = selectedPeer;
|
|
452
|
+
shareBrowser.share = share;
|
|
453
|
+
shareBrowser.path = '';
|
|
454
|
+
requestShareListing(selectedPeer, share, '');
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
const shareSelect = $('share-select');
|
|
459
|
+
const sharePathInput = $('share-path');
|
|
460
|
+
if (shareSelect) {
|
|
461
|
+
shareSelect.addEventListener('change', () => {
|
|
462
|
+
shareBrowser.peer = selectedPeer;
|
|
463
|
+
shareBrowser.share = shareSelect.value;
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
if (sharePathInput) {
|
|
467
|
+
sharePathInput.addEventListener('input', () => {
|
|
468
|
+
shareBrowser.path = sharePathInput.value;
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const btnShareList = $('btn-share-list');
|
|
473
|
+
if (btnShareList) {
|
|
474
|
+
btnShareList.addEventListener('click', () => {
|
|
475
|
+
const share = shareSelect ? shareSelect.value : shareBrowser.share;
|
|
476
|
+
const pathValue = sharePathInput ? sharePathInput.value.trim() : shareBrowser.path;
|
|
477
|
+
requestShareListing(selectedPeer, share, pathValue);
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const btnShareGet = $('btn-share-get');
|
|
482
|
+
if (btnShareGet) {
|
|
483
|
+
btnShareGet.addEventListener('click', () => {
|
|
484
|
+
const share = shareSelect ? shareSelect.value : shareBrowser.share;
|
|
485
|
+
const pathValue = sharePathInput ? sharePathInput.value.trim() : shareBrowser.path;
|
|
486
|
+
requestSharedFile(selectedPeer, share, pathValue);
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
detailsPanel.querySelectorAll('[data-share-open]').forEach((button) => {
|
|
491
|
+
button.addEventListener('click', () => {
|
|
492
|
+
const entryPath = button.getAttribute('data-share-open') || '';
|
|
493
|
+
requestShareListing(selectedPeer, shareBrowser.share, entryPath);
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
detailsPanel.querySelectorAll('[data-share-get]').forEach((button) => {
|
|
498
|
+
button.addEventListener('click', () => {
|
|
499
|
+
const entryPath = button.getAttribute('data-share-get') || '';
|
|
500
|
+
requestSharedFile(selectedPeer, shareBrowser.share, entryPath);
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function sendPeerCommand(target, command, args) {
|
|
506
|
+
if (!connected) {
|
|
507
|
+
addMessage('system', 'System', 'Not connected', Date.now());
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
if (!target) {
|
|
511
|
+
addMessage('system', 'System', 'Select a peer first', Date.now());
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
sendEnvelope('command', { command, args }, { to: target });
|
|
515
|
+
addMessage('command', myIdentity.name + ' -> ' + target, 'CMD: ' + command + (args !== undefined ? ' ' + JSON.stringify(args) : ''), Date.now());
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function requestShareListing(target, share, folderPath) {
|
|
519
|
+
if (!share) {
|
|
520
|
+
addMessage('system', 'System', 'Pick a shared folder first', Date.now());
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
shareBrowser.peer = target;
|
|
524
|
+
shareBrowser.share = share;
|
|
525
|
+
shareBrowser.path = folderPath || '';
|
|
526
|
+
shareBrowser.entries = [];
|
|
527
|
+
shareBrowser.truncated = false;
|
|
528
|
+
sendPeerCommand(target, 'list-files', { share, path: folderPath || '' });
|
|
529
|
+
renderDetails();
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function requestSharedFile(target, share, filePath) {
|
|
533
|
+
if (!share || !filePath) {
|
|
534
|
+
addMessage('system', 'System', 'Choose a share and a file path first', Date.now());
|
|
252
535
|
return;
|
|
253
536
|
}
|
|
254
|
-
|
|
255
|
-
p.skills.map(s => '<span class="skill-tag">' + escHtml(s) + '</span>').join('');
|
|
537
|
+
sendPeerCommand(target, 'get-file', { share, path: filePath });
|
|
256
538
|
}
|
|
257
539
|
|
|
258
540
|
// ── Envelope Protocol ──────────────────────────────────────────────────
|
|
@@ -277,6 +559,144 @@ input:focus,select:focus{border-color:var(--accent)}
|
|
|
277
559
|
} catch(e) { return null; }
|
|
278
560
|
}
|
|
279
561
|
|
|
562
|
+
function handleCommandResponse(env) {
|
|
563
|
+
const payload = env.payload || {};
|
|
564
|
+
const requestId = payload.requestId;
|
|
565
|
+
const pending = requestId ? pendingRequests.get(requestId) : null;
|
|
566
|
+
const result = payload.result || null;
|
|
567
|
+
|
|
568
|
+
if (pending && pending.command === 'shares' && result && Array.isArray(result.shares)) {
|
|
569
|
+
const peer = peers.get(env.from.streamId);
|
|
570
|
+
if (peer) {
|
|
571
|
+
peer.agentProfile = peer.agentProfile || {};
|
|
572
|
+
peer.agentProfile.shares = result.shares;
|
|
573
|
+
}
|
|
574
|
+
if (selectedPeer === env.from.streamId) {
|
|
575
|
+
shareBrowser.peer = env.from.streamId;
|
|
576
|
+
shareBrowser.share = result.shares[0] && result.shares[0].name ? result.shares[0].name : '';
|
|
577
|
+
shareBrowser.path = '';
|
|
578
|
+
shareBrowser.entries = [];
|
|
579
|
+
shareBrowser.truncated = false;
|
|
580
|
+
renderDetails();
|
|
581
|
+
}
|
|
582
|
+
addMessage('command_response', env.from.name || env.from.streamId, 'shares: ' + (result.shares.map((share) => share.name).join(', ') || 'none'), env.ts);
|
|
583
|
+
return true;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (pending && pending.command === 'list-files' && result && Array.isArray(result.entries)) {
|
|
587
|
+
shareBrowser.peer = env.from.streamId;
|
|
588
|
+
shareBrowser.share = result.share || (pending.args && pending.args.share) || '';
|
|
589
|
+
shareBrowser.path = result.path || '';
|
|
590
|
+
shareBrowser.entries = result.entries;
|
|
591
|
+
shareBrowser.truncated = Boolean(result.truncated);
|
|
592
|
+
if (selectedPeer === env.from.streamId) {
|
|
593
|
+
renderDetails();
|
|
594
|
+
}
|
|
595
|
+
addMessage('command_response', env.from.name || env.from.streamId, 'listed ' + result.share + (result.path ? '/' + result.path : '') + ' (' + result.entries.length + ' entries)', env.ts);
|
|
596
|
+
return true;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (pending && pending.command === 'get-file' && payload.ok && result && result.name) {
|
|
600
|
+
addMessage('command_response', env.from.name || env.from.streamId, 'sending ' + result.name + ' from ' + result.share + '/' + result.path, env.ts);
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (pending && payload.ok === false && payload.error) {
|
|
605
|
+
addMessage('command_response', env.from.name || env.from.streamId, pending.command + ' failed: ' + payload.error, env.ts);
|
|
606
|
+
return true;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function handleIncomingFileOffer(env) {
|
|
613
|
+
const payload = env.payload || {};
|
|
614
|
+
incomingTransfers.set(payload.transferId, {
|
|
615
|
+
from: env.from,
|
|
616
|
+
name: payload.name || 'download',
|
|
617
|
+
mimeType: payload.mimeType || 'application/octet-stream',
|
|
618
|
+
transferKind: payload.kind || 'file',
|
|
619
|
+
size: payload.size || 0,
|
|
620
|
+
sha256: payload.sha256 || '',
|
|
621
|
+
totalChunks: payload.totalChunks || 0,
|
|
622
|
+
receivedBytes: 0,
|
|
623
|
+
parts: new Array(payload.totalChunks || 0),
|
|
624
|
+
});
|
|
625
|
+
addMessage('event', env.from.name || env.from.streamId, 'sending ' + (payload.name || 'file'), env.ts, payload.kind || 'file');
|
|
626
|
+
return true;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function handleIncomingFileChunk(env) {
|
|
630
|
+
const payload = env.payload || {};
|
|
631
|
+
const transfer = incomingTransfers.get(payload.transferId);
|
|
632
|
+
if (!transfer) return true;
|
|
633
|
+
const bytes = bytesFromBase64(payload.data || '');
|
|
634
|
+
transfer.parts[payload.index] = bytes;
|
|
635
|
+
transfer.receivedBytes += bytes.length;
|
|
636
|
+
return true;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
async function handleIncomingFileComplete(env) {
|
|
640
|
+
const payload = env.payload || {};
|
|
641
|
+
const transfer = incomingTransfers.get(payload.transferId);
|
|
642
|
+
if (!transfer) return true;
|
|
643
|
+
|
|
644
|
+
try {
|
|
645
|
+
const missing = transfer.parts.some((part) => !part);
|
|
646
|
+
if (missing) throw new Error('file transfer incomplete');
|
|
647
|
+
if (transfer.receivedBytes !== transfer.size || payload.size !== transfer.size) {
|
|
648
|
+
throw new Error('file size mismatch');
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const bytes = concatBytes(transfer.parts, transfer.size);
|
|
652
|
+
const sha = await sha256Hex(bytes);
|
|
653
|
+
if (transfer.sha256 && sha !== transfer.sha256) {
|
|
654
|
+
throw new Error('file checksum mismatch');
|
|
655
|
+
}
|
|
656
|
+
if (payload.sha256 && sha !== payload.sha256) {
|
|
657
|
+
throw new Error('file checksum mismatch');
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const blob = new Blob([bytes], { type: transfer.mimeType });
|
|
661
|
+
const url = URL.createObjectURL(blob);
|
|
662
|
+
sendEnvelope('file_ack', {
|
|
663
|
+
transferId: payload.transferId,
|
|
664
|
+
ok: true,
|
|
665
|
+
name: transfer.name,
|
|
666
|
+
mimeType: transfer.mimeType,
|
|
667
|
+
kind: transfer.transferKind,
|
|
668
|
+
size: transfer.size,
|
|
669
|
+
sha256: sha
|
|
670
|
+
}, { to: env.from.streamId });
|
|
671
|
+
|
|
672
|
+
addMessage(
|
|
673
|
+
'event',
|
|
674
|
+
env.from.name || env.from.streamId,
|
|
675
|
+
'received ' + transfer.name,
|
|
676
|
+
env.ts,
|
|
677
|
+
transfer.transferKind + ' ready',
|
|
678
|
+
{ downloadUrl: url, downloadName: transfer.name },
|
|
679
|
+
);
|
|
680
|
+
} catch (err) {
|
|
681
|
+
sendEnvelope('file_ack', {
|
|
682
|
+
transferId: payload.transferId,
|
|
683
|
+
ok: false,
|
|
684
|
+
error: err && err.message ? err.message : String(err)
|
|
685
|
+
}, { to: env.from.streamId });
|
|
686
|
+
addMessage('system', 'System', 'File receive failed: ' + (err && err.message ? err.message : String(err)), Date.now());
|
|
687
|
+
} finally {
|
|
688
|
+
incomingTransfers.delete(payload.transferId);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
return true;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function handleIncomingFileAck(env) {
|
|
695
|
+
const payload = env.payload || {};
|
|
696
|
+
addMessage('event', env.from.name || env.from.streamId, (payload.ok ? 'delivery confirmed for ' : 'delivery failed for ') + (payload.name || payload.transferId), env.ts);
|
|
697
|
+
return true;
|
|
698
|
+
}
|
|
699
|
+
|
|
280
700
|
// ── Connection ─────────────────────────────────────────────────────────
|
|
281
701
|
async function doConnect() {
|
|
282
702
|
const room = inpRoom.value.trim();
|
|
@@ -294,9 +714,9 @@ input:focus,select:focus{border-color:var(--accent)}
|
|
|
294
714
|
};
|
|
295
715
|
|
|
296
716
|
// Save to localStorage
|
|
297
|
-
localStorage.setItem('
|
|
298
|
-
localStorage.setItem('
|
|
299
|
-
localStorage.setItem('
|
|
717
|
+
localStorage.setItem('ninja_room', room);
|
|
718
|
+
localStorage.setItem('ninja_password', inpPassword.value.trim());
|
|
719
|
+
localStorage.setItem('ninja_name', name);
|
|
300
720
|
|
|
301
721
|
statusEl.textContent = 'Connecting...';
|
|
302
722
|
btnConnect.disabled = true;
|
|
@@ -311,13 +731,7 @@ input:focus,select:focus{border-color:var(--accent)}
|
|
|
311
731
|
sdk.addEventListener('peerConnected', (e) => {
|
|
312
732
|
const uuid = e.detail?.uuid || 'unknown';
|
|
313
733
|
const sid = e.detail?.streamID || uuid;
|
|
314
|
-
|
|
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
|
-
}
|
|
734
|
+
upsertPeer(sid, uuid, null);
|
|
321
735
|
renderPeers();
|
|
322
736
|
});
|
|
323
737
|
|
|
@@ -327,8 +741,19 @@ input:focus,select:focus{border-color:var(--accent)}
|
|
|
327
741
|
const announce = createEnvelope('announce', {
|
|
328
742
|
skills: ['dashboard-ui', 'chat'],
|
|
329
743
|
status: 'online',
|
|
330
|
-
version:
|
|
331
|
-
topics: ['events', 'bots', 'support']
|
|
744
|
+
version: DASHBOARD_VERSION,
|
|
745
|
+
topics: ['events', 'bots', 'support'],
|
|
746
|
+
agent: {
|
|
747
|
+
runtime: 'dashboard-ui',
|
|
748
|
+
summary: 'Browser operator console for room chat, bot inspection, shared-folder browsing, and file downloads',
|
|
749
|
+
can: ['chat', 'direct-message', 'command', 'operator', 'browse-shares', 'download-file'],
|
|
750
|
+
asks: [
|
|
751
|
+
{ name: 'status', description: 'Ask connected bots for current status' },
|
|
752
|
+
{ name: 'peers', description: 'Ask connected bots for peer lists' },
|
|
753
|
+
{ name: 'capabilities', description: 'Ask a selected bot what it can do' },
|
|
754
|
+
{ name: 'shares', description: 'Ask a selected bot which shared folders it exposes' }
|
|
755
|
+
]
|
|
756
|
+
}
|
|
332
757
|
});
|
|
333
758
|
try { sdk.sendData(announce, { UUID: uuid }); } catch(err) {}
|
|
334
759
|
});
|
|
@@ -340,10 +765,9 @@ input:focus,select:focus{border-color:var(--accent)}
|
|
|
340
765
|
if (!env) return;
|
|
341
766
|
|
|
342
767
|
const sender = env.from.streamId;
|
|
768
|
+
if (!sender || sender === myStreamId) return;
|
|
343
769
|
// Update peer info
|
|
344
|
-
|
|
345
|
-
peers.set(sender, { uuid, identity: env.from, skills: [], status: 'online', connected: true });
|
|
346
|
-
}
|
|
770
|
+
upsertPeer(sender, uuid, env.from);
|
|
347
771
|
|
|
348
772
|
switch (env.type) {
|
|
349
773
|
case 'announce': {
|
|
@@ -352,12 +776,16 @@ input:focus,select:focus{border-color:var(--accent)}
|
|
|
352
776
|
p.identity = env.from;
|
|
353
777
|
p.skills = env.payload?.skills || [];
|
|
354
778
|
p.status = env.payload?.status || 'online';
|
|
779
|
+
p.statusDetail = env.payload?.statusDetail || '';
|
|
780
|
+
p.topics = env.payload?.topics || [];
|
|
781
|
+
p.version = env.payload?.version || '';
|
|
782
|
+
p.agentProfile = env.payload?.agent || null;
|
|
355
783
|
}
|
|
356
784
|
addMessage('announce', env.from.name || sender,
|
|
357
785
|
`joined as ${env.from.role || '?'} with skills: ${(env.payload?.skills || []).join(', ')}`,
|
|
358
786
|
env.ts);
|
|
359
787
|
renderPeers();
|
|
360
|
-
|
|
788
|
+
renderDetails();
|
|
361
789
|
break;
|
|
362
790
|
}
|
|
363
791
|
case 'skill_update': {
|
|
@@ -365,12 +793,14 @@ input:focus,select:focus{border-color:var(--accent)}
|
|
|
365
793
|
if (p) {
|
|
366
794
|
p.skills = env.payload?.skills || p.skills;
|
|
367
795
|
p.status = env.payload?.status || p.status;
|
|
796
|
+
p.statusDetail = env.payload?.statusDetail || p.statusDetail || '';
|
|
797
|
+
p.agentProfile = env.payload?.agent || p.agentProfile || null;
|
|
368
798
|
}
|
|
369
799
|
addMessage('event', env.from.name || sender,
|
|
370
800
|
`status: ${env.payload?.status || '?'}${env.payload?.statusDetail ? ' - ' + env.payload.statusDetail : ''}`,
|
|
371
801
|
env.ts);
|
|
372
802
|
renderPeers();
|
|
373
|
-
|
|
803
|
+
renderDetails();
|
|
374
804
|
break;
|
|
375
805
|
}
|
|
376
806
|
case 'chat':
|
|
@@ -391,9 +821,23 @@ input:focus,select:focus{border-color:var(--accent)}
|
|
|
391
821
|
env.ts);
|
|
392
822
|
break;
|
|
393
823
|
case 'command_response':
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
824
|
+
if (!handleCommandResponse(env)) {
|
|
825
|
+
addMessage('command_response', env.from.name || sender,
|
|
826
|
+
JSON.stringify(env.payload, null, 2),
|
|
827
|
+
env.ts);
|
|
828
|
+
}
|
|
829
|
+
break;
|
|
830
|
+
case 'file_offer':
|
|
831
|
+
handleIncomingFileOffer(env);
|
|
832
|
+
break;
|
|
833
|
+
case 'file_chunk':
|
|
834
|
+
handleIncomingFileChunk(env);
|
|
835
|
+
break;
|
|
836
|
+
case 'file_complete':
|
|
837
|
+
void handleIncomingFileComplete(env);
|
|
838
|
+
break;
|
|
839
|
+
case 'file_ack':
|
|
840
|
+
handleIncomingFileAck(env);
|
|
397
841
|
break;
|
|
398
842
|
case 'ping':
|
|
399
843
|
// Auto-respond with pong
|
|
@@ -426,7 +870,7 @@ input:focus,select:focus{border-color:var(--accent)}
|
|
|
426
870
|
// Find by uuid if no streamID
|
|
427
871
|
let key = sid;
|
|
428
872
|
if (!key) {
|
|
429
|
-
|
|
873
|
+
key = getPeerKeyByUuid(uuid);
|
|
430
874
|
}
|
|
431
875
|
if (key && peers.has(key)) {
|
|
432
876
|
const p = peers.get(key);
|
|
@@ -434,6 +878,7 @@ input:focus,select:focus{border-color:var(--accent)}
|
|
|
434
878
|
p.connected = false;
|
|
435
879
|
addMessage('system', name, 'disconnected', Date.now());
|
|
436
880
|
renderPeers();
|
|
881
|
+
renderDetails();
|
|
437
882
|
}
|
|
438
883
|
});
|
|
439
884
|
|
|
@@ -503,6 +948,7 @@ input:focus,select:focus{border-color:var(--accent)}
|
|
|
503
948
|
inpRoom.disabled = false;
|
|
504
949
|
inpPassword.disabled = false;
|
|
505
950
|
renderPeers();
|
|
951
|
+
renderDetails();
|
|
506
952
|
addMessage('system', 'System', 'Disconnected', Date.now());
|
|
507
953
|
}
|
|
508
954
|
|
|
@@ -519,7 +965,7 @@ input:focus,select:focus{border-color:var(--accent)}
|
|
|
519
965
|
switch (cmd) {
|
|
520
966
|
case 'help':
|
|
521
967
|
addMessage('system', 'System',
|
|
522
|
-
'Commands: /help, /peers, /status, /history [n], /health, /clear, /dm <peer> <msg>',
|
|
968
|
+
'Commands: /help, /peers, /status, /history [n], /health, /clear, /dm <peer> <msg>, /cmd <peer> <command> [json], /plan <peer> <request>, /review <peer> <request>, /approve <peer> <request>, /respond <peer> <requestId> [json], /profile [peer], /capabilities [peer], /inbox [peer], /shares [peer], /ls <peer> <share> [path], /get <peer> <share> <path>',
|
|
523
969
|
Date.now());
|
|
524
970
|
return;
|
|
525
971
|
case 'peers':
|
|
@@ -543,13 +989,97 @@ input:focus,select:focus{border-color:var(--accent)}
|
|
|
543
989
|
messagesEl.innerHTML = '';
|
|
544
990
|
messages.length = 0;
|
|
545
991
|
return;
|
|
546
|
-
case 'dm':
|
|
992
|
+
case 'dm': {
|
|
547
993
|
if (parts.length < 3) { addMessage('system', 'System', 'Usage: /dm <peer_stream_id> <message>', Date.now()); return; }
|
|
548
994
|
const target = parts[1];
|
|
549
995
|
const msg = parts.slice(2).join(' ');
|
|
550
996
|
sendEnvelope('chat', { text: msg }, { to: target });
|
|
551
997
|
addMessage('chat', myIdentity.name + ' -> ' + target, msg, Date.now());
|
|
552
998
|
return;
|
|
999
|
+
}
|
|
1000
|
+
case 'cmd': {
|
|
1001
|
+
const target = parts[1] || selectedPeer;
|
|
1002
|
+
if (!target || parts.length < 3) {
|
|
1003
|
+
addMessage('system', 'System', 'Usage: /cmd <peer_stream_id> <command> [json]', Date.now());
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
const command = parts[2];
|
|
1007
|
+
let args = undefined;
|
|
1008
|
+
if (parts.length > 3) {
|
|
1009
|
+
const raw = parts.slice(3).join(' ');
|
|
1010
|
+
try { args = JSON.parse(raw); } catch(_err) { args = raw; }
|
|
1011
|
+
}
|
|
1012
|
+
sendEnvelope('command', { command, args }, { to: target });
|
|
1013
|
+
addMessage('command', myIdentity.name + ' -> ' + target, 'CMD: ' + command + (args !== undefined ? ' ' + JSON.stringify(args) : ''), Date.now());
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
case 'shares': {
|
|
1017
|
+
const target = parts[1] || selectedPeer;
|
|
1018
|
+
if (!target) {
|
|
1019
|
+
addMessage('system', 'System', 'Select a peer or use /shares <peer_stream_id>', Date.now());
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
sendPeerCommand(target, 'shares');
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
case 'ls': {
|
|
1026
|
+
const target = parts[1] || selectedPeer;
|
|
1027
|
+
if (!target || parts.length < 3) {
|
|
1028
|
+
addMessage('system', 'System', 'Usage: /ls <peer_stream_id> <share> [path]', Date.now());
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
requestShareListing(target, parts[2], parts.slice(3).join(' '));
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
case 'get': {
|
|
1035
|
+
const target = parts[1] || selectedPeer;
|
|
1036
|
+
if (!target || parts.length < 4) {
|
|
1037
|
+
addMessage('system', 'System', 'Usage: /get <peer_stream_id> <share> <path>', Date.now());
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
requestSharedFile(target, parts[2], parts.slice(3).join(' '));
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
case 'plan':
|
|
1044
|
+
case 'review':
|
|
1045
|
+
case 'approve': {
|
|
1046
|
+
const target = parts[1] || selectedPeer;
|
|
1047
|
+
if (!target || parts.length < 3) {
|
|
1048
|
+
addMessage('system', 'System', 'Usage: /' + cmd + ' <peer_stream_id> <request>', Date.now());
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
const request = parts.slice(2).join(' ');
|
|
1052
|
+
sendPeerCommand(target, cmd, { request });
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
case 'respond': {
|
|
1056
|
+
const target = parts[1] || selectedPeer;
|
|
1057
|
+
if (!target || parts.length < 3) {
|
|
1058
|
+
addMessage('system', 'System', 'Usage: /respond <peer_stream_id> <requestId> [json]', Date.now());
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
const requestId = parts[2];
|
|
1062
|
+
let result = null;
|
|
1063
|
+
if (parts.length > 3) {
|
|
1064
|
+
const raw = parts.slice(3).join(' ');
|
|
1065
|
+
try { result = JSON.parse(raw); } catch (_err) { result = raw; }
|
|
1066
|
+
}
|
|
1067
|
+
sendEnvelope('command_response', { requestId, ok: true, result }, { to: target });
|
|
1068
|
+
addMessage('command', myIdentity.name + ' -> ' + target, 'RESP: ' + requestId + ' ' + JSON.stringify(result), Date.now());
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
case 'profile':
|
|
1072
|
+
case 'whoami':
|
|
1073
|
+
case 'capabilities':
|
|
1074
|
+
case 'inbox': {
|
|
1075
|
+
const target = parts[1] || selectedPeer;
|
|
1076
|
+
if (!target) {
|
|
1077
|
+
addMessage('system', 'System', 'Select a peer or use /' + cmd + ' <peer_stream_id>', Date.now());
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
sendPeerCommand(target, cmd);
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
553
1083
|
default:
|
|
554
1084
|
addMessage('system', 'System', 'Unknown command: /' + cmd + '. Try /help', Date.now());
|
|
555
1085
|
return;
|
|
@@ -558,14 +1088,22 @@ input:focus,select:focus{border-color:var(--accent)}
|
|
|
558
1088
|
|
|
559
1089
|
// Regular chat message
|
|
560
1090
|
if (!connected) { addMessage('system', 'System', 'Not connected', Date.now()); return; }
|
|
561
|
-
const
|
|
562
|
-
sendEnvelope('chat', { text: text },
|
|
563
|
-
addMessage('chat', myIdentity.name + (
|
|
1091
|
+
const broadcastTarget = chatTarget.value || null;
|
|
1092
|
+
sendEnvelope('chat', { text: text }, broadcastTarget ? { to: broadcastTarget } : undefined);
|
|
1093
|
+
addMessage('chat', myIdentity.name + (broadcastTarget ? ' -> ' + broadcastTarget : ''), text, Date.now());
|
|
564
1094
|
}
|
|
565
1095
|
|
|
566
1096
|
function sendEnvelope(type, payload, opts) {
|
|
567
1097
|
if (!sdk || !connected) return;
|
|
568
1098
|
const env = createEnvelope(type, payload, opts);
|
|
1099
|
+
if (type === 'command') {
|
|
1100
|
+
pendingRequests.set(env.id, {
|
|
1101
|
+
command: payload && payload.command,
|
|
1102
|
+
target: opts && opts.to ? opts.to : null,
|
|
1103
|
+
args: payload && payload.args !== undefined ? payload.args : undefined,
|
|
1104
|
+
createdAt: env.ts
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
569
1107
|
try {
|
|
570
1108
|
if (opts && opts.to) {
|
|
571
1109
|
// Find the peer's UUID for targeted send
|
|
@@ -590,7 +1128,8 @@ input:focus,select:focus{border-color:var(--accent)}
|
|
|
590
1128
|
chatInput.addEventListener('keydown', (e) => {
|
|
591
1129
|
if (e.key === 'Enter') { e.preventDefault(); handleInput(); }
|
|
592
1130
|
});
|
|
593
|
-
chatTarget.addEventListener('change', () => { selectedPeer = chatTarget.value; renderPeers(); });
|
|
1131
|
+
chatTarget.addEventListener('change', () => { selectedPeer = chatTarget.value; renderPeers(); renderDetails(); });
|
|
1132
|
+
renderDetails();
|
|
594
1133
|
|
|
595
1134
|
// Auto-connect if room is provided via URL params
|
|
596
1135
|
if (params.get('room') && params.get('autoconnect') !== 'false') {
|