vibedate 0.4.0 → 0.4.1

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/dist/cli.d.ts CHANGED
@@ -9,7 +9,7 @@ interface ParsedArgs {
9
9
  readonly live: boolean;
10
10
  /** `live --dating`: pick-a-handle mode vs omegle auto-pair. Default false. */
11
11
  readonly dating: boolean;
12
- /** `discover --any` / `live --any`: match every league (not just ±1). Default false. */
12
+ /** `discover --any` / `live --any` / `open --any`: match every league (not just ±1). Default false. */
13
13
  readonly any: boolean;
14
14
  /** Positional argument for `handle`/`find` (e.g. `@name`). */
15
15
  readonly arg: string | undefined;
package/dist/cli.js CHANGED
@@ -110,6 +110,7 @@ function createPairing() {
110
110
 
111
111
  // src/server.ts
112
112
  import http from "http";
113
+ import { randomUUID } from "crypto";
113
114
  import { makeEvent, notify as vibeCoreNotify } from "@pooriaarab/vibe-core";
114
115
 
115
116
  // src/web-app-html.ts
@@ -545,6 +546,80 @@ var webAppHtml = `<!DOCTYPE html>
545
546
  background: var(--danger); color: #2a0a0c;
546
547
  }
547
548
  .video-modal .vhangup:hover{ filter: brightness(1.06); }
549
+ .live-panel .lp-legend{ font-size: .66rem; color: var(--muted-2); margin-top: 8px; line-height: 1.4; }
550
+ .incoming-call{
551
+ position: fixed; inset: 0; z-index: 65;
552
+ display: flex; align-items: center; justify-content: center;
553
+ background: rgba(12,7,15,.6); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
554
+ opacity: 0; pointer-events: none; transition: opacity var(--dur-med) ease;
555
+ }
556
+ .incoming-call.is-open{ opacity: 1; pointer-events: auto; }
557
+ .incoming-call .ic-card{
558
+ background: linear-gradient(165deg, var(--bg-card), var(--bg-card-2));
559
+ border: 1px solid var(--border-2); border-radius: 24px; box-shadow: var(--shadow-3);
560
+ padding: 30px 28px; max-width: 320px; text-align: center;
561
+ transform: scale(.85) translateY(10px); transition: transform var(--dur-med) var(--ease-bounce);
562
+ }
563
+ .incoming-call.is-open .ic-card{ transform: scale(1) translateY(0); }
564
+ .incoming-call h3{ margin: 0 0 6px; font-size: 1.15rem; font-weight: 800; word-break: break-all; }
565
+ .incoming-call p{ color: var(--muted); font-size: .85rem; margin: 0 0 18px; }
566
+ .incoming-call .ic-actions{ display: flex; gap: 10px; }
567
+
568
+ /* ---- live text chat (browser <-> PeerLink over /live/message) ---- */
569
+ .live-actions{ display: flex; gap: 6px; flex-shrink: 0; }
570
+ .live-row .cbtn{
571
+ border: 1px solid var(--border-2); border-radius: 9px; padding: 7px 12px;
572
+ font-weight: 700; font-size: .76rem; background: transparent; color: var(--muted);
573
+ transition: color var(--dur-fast) ease, border-color var(--dur-fast) ease, transform var(--dur-fast) var(--ease-out);
574
+ flex-shrink: 0;
575
+ }
576
+ .live-row .cbtn:hover{ color: var(--fg); border-color: var(--muted-2); }
577
+ .live-row .cbtn:active{ transform: scale(.95); }
578
+ .live-row .cbtn.has-unread{ color: var(--coral); border-color: var(--coral); }
579
+ .chat-panel{
580
+ position: fixed; right: 18px; bottom: 18px; z-index: 45;
581
+ width: min(320px, calc(100vw - 36px));
582
+ background: linear-gradient(180deg, var(--bg-card), var(--bg-card-2));
583
+ border: 1px solid var(--border-2); border-radius: 16px; box-shadow: var(--shadow-2);
584
+ display: none; flex-direction: column; overflow: hidden;
585
+ }
586
+ .chat-panel.is-open{ display: flex; animation: rise var(--dur-med) var(--ease-out); }
587
+ .chat-panel .cp-head{
588
+ display: flex; align-items: center; justify-content: space-between; gap: 10px;
589
+ padding: 11px 14px; border-bottom: 1px solid var(--border);
590
+ }
591
+ .chat-panel .cp-title{ font-size: .84rem; font-weight: 700; word-break: break-all; }
592
+ .chat-panel .cp-sub{ font-size: .68rem; color: var(--muted-2); margin-top: 2px; }
593
+ .chat-panel .cp-close{
594
+ border: 0; background: transparent; color: var(--muted-2);
595
+ font-size: 1.15rem; line-height: 1; padding: 4px; flex-shrink: 0;
596
+ }
597
+ .chat-panel .cp-close:hover{ color: var(--fg); }
598
+ .chat-panel .cp-msgs{
599
+ padding: 12px 14px; min-height: 120px; max-height: 260px; overflow-y: auto;
600
+ display: flex; flex-direction: column; gap: 7px;
601
+ }
602
+ .chat-panel .cp-empty{ font-size: .76rem; color: var(--muted-2); text-align: center; margin: auto; }
603
+ .cp-msg{
604
+ max-width: 85%; padding: 7px 11px; border-radius: 13px;
605
+ font-size: .82rem; line-height: 1.4; word-break: break-word; white-space: pre-wrap;
606
+ }
607
+ .cp-msg.them{ align-self: flex-start; background: rgba(248,239,232,.07); border: 1px solid var(--border); }
608
+ .cp-msg.you{ align-self: flex-end; background: rgba(255,122,104,.16); border: 1px solid rgba(255,122,104,.32); }
609
+ .cp-msg.sys{ align-self: center; background: transparent; border: 0; color: var(--muted-2); font-size: .72rem; padding: 2px 6px; }
610
+ .chat-panel .cp-inputrow{ display: flex; gap: 8px; padding: 10px; border-top: 1px solid var(--border); }
611
+ .chat-panel .cp-inputrow input{
612
+ flex: 1; border: 1px solid var(--border-2); border-radius: 10px; background: rgba(0,0,0,.22);
613
+ color: var(--fg); padding: 9px 11px; font: inherit; font-size: .82rem; outline: none; min-width: 0;
614
+ }
615
+ .chat-panel .cp-inputrow input:focus{ border-color: var(--coral); }
616
+ .chat-panel .cp-send{
617
+ border: 0; border-radius: 10px; padding: 9px 14px; font-weight: 700; font-size: .8rem;
618
+ background: linear-gradient(180deg, var(--coral), var(--coral-dim)); color: #2a1109;
619
+ transition: filter var(--dur-fast) ease, transform var(--dur-fast) var(--ease-out);
620
+ }
621
+ .chat-panel .cp-send:hover{ filter: brightness(1.06); }
622
+ .chat-panel .cp-send:active{ transform: scale(.95); }
548
623
  </style>
549
624
  </head>
550
625
  <body>
@@ -689,9 +764,36 @@ var webAppHtml = `<!DOCTYPE html>
689
764
  </div>
690
765
  </div>
691
766
 
692
- <aside class="live-panel" id="livePanel" aria-label="Live same-league peers">
693
- <div class="lp-title"><span class="lp-dot" aria-hidden="true"></span> Live same-league peers</div>
767
+ <aside class="live-panel" id="livePanel" aria-label="Live peers">
768
+ <div class="lp-title"><span class="lp-dot" aria-hidden="true"></span> Live peers</div>
694
769
  <div id="liveRows"></div>
770
+ <div class="lp-legend">&#10003; usage verified &middot; &#128273; identity verified &mdash; call someone, or wait for a call</div>
771
+ </aside>
772
+
773
+ <div class="incoming-call" id="incomingCall" role="dialog" aria-modal="true" aria-label="Incoming call">
774
+ <div class="ic-card">
775
+ <h3 id="incomingCaller">@peer</h3>
776
+ <p>is calling you &mdash; video chat</p>
777
+ <div class="ic-actions">
778
+ <button class="btn btn-ghost" id="declineBtn" type="button">Decline</button>
779
+ <button class="btn btn-primary" id="acceptBtn" type="button">Accept</button>
780
+ </div>
781
+ </div>
782
+ </div>
783
+
784
+ <aside class="chat-panel" id="chatPanel" aria-label="Text chat">
785
+ <div class="cp-head">
786
+ <div>
787
+ <div class="cp-title" id="chatTitle">@peer</div>
788
+ <div class="cp-sub" id="chatSub">live over the P2P link &middot; text only</div>
789
+ </div>
790
+ <button class="cp-close" id="chatClose" type="button" aria-label="Close chat">&times;</button>
791
+ </div>
792
+ <div class="cp-msgs" id="chatMsgs"></div>
793
+ <div class="cp-inputrow">
794
+ <input id="chatInput" type="text" maxlength="4000" placeholder="message&hellip;" autocomplete="off">
795
+ <button class="cp-send" id="chatSend" type="button">Send</button>
796
+ </div>
695
797
  </aside>
696
798
 
697
799
  <div class="video-modal" id="videoModal" role="dialog" aria-modal="true" aria-label="Video call">
@@ -998,9 +1100,15 @@ var webAppHtml = `<!DOCTYPE html>
998
1100
  var remoteVideo = document.getElementById("remoteVideo");
999
1101
  var remoteLabel = document.getElementById("remoteLabel");
1000
1102
  var hangupBtn = document.getElementById("hangupBtn");
1103
+ var incomingCallEl = document.getElementById("incomingCall");
1104
+ var incomingCaller = document.getElementById("incomingCaller");
1105
+ var acceptBtn = document.getElementById("acceptBtn");
1106
+ var declineBtn = document.getElementById("declineBtn");
1001
1107
 
1002
1108
  // One in-flight call's state. remoteHandle === null means idle.
1003
1109
  var rtc = { pc: null, localStream: null, remoteHandle: null, role: null };
1110
+ // An incoming offer awaiting the user's accept/decline (never auto-answered).
1111
+ var pendingOffer = null;
1004
1112
  var knownPeers = [];
1005
1113
  var idleIdx = 0;
1006
1114
 
@@ -1117,7 +1225,8 @@ var webAppHtml = `<!DOCTYPE html>
1117
1225
  }
1118
1226
 
1119
1227
  // Idle listener: when not in a call, watch known peers round-robin so an
1120
- // incoming offer (the peer clicked THEIR Video button) is noticed and answered.
1228
+ // incoming offer (the peer clicked THEIR Call button) is noticed. Offers are
1229
+ // never auto-answered \u2014 the caller is named and the user accepts or declines.
1121
1230
  async function idleLoop(){
1122
1231
  while (true) {
1123
1232
  if (rtc.remoteHandle || knownPeers.length === 0) { await sleep(1000); continue; }
@@ -1129,7 +1238,12 @@ var webAppHtml = `<!DOCTYPE html>
1129
1238
  var data = await res.json();
1130
1239
  var f = data && data.frame;
1131
1240
  if (f && f.t === "rtc-offer" && !rtc.pc && !rtc.remoteHandle) {
1132
- await answerIncomingOffer(peer.handle, f.sdp);
1241
+ // One prompt at a time: a re-offer from the SAME caller refreshes
1242
+ // the stored sdp; an offer from someone else while a prompt is up
1243
+ // is dropped (their call simply fails to connect).
1244
+ if (pendingOffer && pendingOffer.handle !== peer.handle) continue;
1245
+ pendingOffer = { handle: peer.handle, sdp: f.sdp };
1246
+ showIncomingPrompt(peer.handle);
1133
1247
  }
1134
1248
  }
1135
1249
  } catch(e){ /* abort/timeout \u2014 loop to next peer */ }
@@ -1137,6 +1251,22 @@ var webAppHtml = `<!DOCTYPE html>
1137
1251
  }
1138
1252
  idleLoop();
1139
1253
 
1254
+ function showIncomingPrompt(handle){
1255
+ incomingCaller.textContent = handle;
1256
+ incomingCallEl.classList.add("is-open");
1257
+ }
1258
+ function clearIncomingPrompt(){
1259
+ pendingOffer = null;
1260
+ incomingCallEl.classList.remove("is-open");
1261
+ }
1262
+ acceptBtn.addEventListener("click", function(){
1263
+ var p = pendingOffer;
1264
+ clearIncomingPrompt();
1265
+ if (!p) return;
1266
+ answerIncomingOffer(p.handle, p.sdp).catch(function(){ hangup(); });
1267
+ });
1268
+ declineBtn.addEventListener("click", clearIncomingPrompt);
1269
+
1140
1270
  function hangup(){
1141
1271
  try { if (rtc.pc) rtc.pc.close(); } catch(e){}
1142
1272
  rtc.pc = null;
@@ -1152,7 +1282,171 @@ var webAppHtml = `<!DOCTYPE html>
1152
1282
  }
1153
1283
  hangupBtn.addEventListener("click", hangup);
1154
1284
 
1155
- // Render the live-peers panel with a Video button each.
1285
+ /* ---- Live text chat: msg frames over the same PeerLinks, via /live/message ----
1286
+ * One long-poll loop per connected peer surfaces incoming texts immediately,
1287
+ * no matter which conversation is open. Outbound texts POST {handle, text};
1288
+ * the server re-validates through the frame allowlist and mints id/at. */
1289
+ var chatPanel = document.getElementById("chatPanel");
1290
+ var chatTitle = document.getElementById("chatTitle");
1291
+ var chatSub = document.getElementById("chatSub");
1292
+ var chatMsgs = document.getElementById("chatMsgs");
1293
+ var chatInput = document.getElementById("chatInput");
1294
+ var chatSend = document.getElementById("chatSend");
1295
+ var chatClose = document.getElementById("chatClose");
1296
+
1297
+ var chatWith = null; // handle of the open conversation (null = closed)
1298
+ var conversations = {}; // handle -> [{from:"you"|"them"|"sys", text}]
1299
+ var unread = {}; // handle -> count of unseen incoming messages
1300
+ var chatLoops = {}; // handle -> true while its poll loop is running
1301
+ var MAX_CHAT_KEPT = 200; // per-conversation local cap (mirrors the server)
1302
+
1303
+ function fetchMessage(handle, timeoutMs){
1304
+ var ctrl = new AbortController();
1305
+ var t = setTimeout(function(){ ctrl.abort(); }, timeoutMs);
1306
+ return fetch("/live/message?handle=" + encodeURIComponent(handle), { signal: ctrl.signal })
1307
+ .then(function(r){ clearTimeout(t); return r; })
1308
+ .catch(function(e){ clearTimeout(t); throw e; });
1309
+ }
1310
+
1311
+ function postChat(handle, text){
1312
+ return fetch("/live/message", {
1313
+ method: "POST",
1314
+ headers: { "content-type": "application/json" },
1315
+ body: JSON.stringify({ handle: handle, text: text })
1316
+ });
1317
+ }
1318
+
1319
+ function peerKnown(handle){
1320
+ for (var i = 0; i < knownPeers.length; i++){ if (knownPeers[i].handle === handle) return true; }
1321
+ return false;
1322
+ }
1323
+
1324
+ function pushChat(handle, entry){
1325
+ var conv = conversations[handle] || (conversations[handle] = []);
1326
+ conv.push(entry);
1327
+ if (conv.length > MAX_CHAT_KEPT) conv.splice(0, conv.length - MAX_CHAT_KEPT);
1328
+ }
1329
+
1330
+ // One long-poll loop per connected peer: started when the peer is first
1331
+ // seen, stopped when it vanishes (its mailbox on the server is gone).
1332
+ function ensureChatLoop(handle){
1333
+ if (chatLoops[handle]) return;
1334
+ chatLoops[handle] = true;
1335
+ (async function(){
1336
+ while (peerKnown(handle)) {
1337
+ var res;
1338
+ try { res = await fetchMessage(handle, 30000); }
1339
+ catch(e){ await sleep(1000); continue; }
1340
+ if (!res || res.status !== 200) { await sleep(1000); continue; }
1341
+ var data = await res.json();
1342
+ var m = data && data.message;
1343
+ if (!m) continue; // timed out empty
1344
+ pushChat(handle, { from: "them", text: m.text });
1345
+ if (chatWith === handle) {
1346
+ renderChat();
1347
+ } else {
1348
+ unread[handle] = (unread[handle] || 0) + 1;
1349
+ renderLiveRows(knownPeers);
1350
+ }
1351
+ }
1352
+ delete chatLoops[handle];
1353
+ })();
1354
+ }
1355
+
1356
+ function renderChat(){
1357
+ chatMsgs.innerHTML = "";
1358
+ var conv = (chatWith && conversations[chatWith]) || [];
1359
+ if (conv.length === 0) {
1360
+ var empty = document.createElement("div");
1361
+ empty.className = "cp-empty";
1362
+ empty.textContent = "No messages yet \u2014 say hi.";
1363
+ chatMsgs.appendChild(empty);
1364
+ return;
1365
+ }
1366
+ conv.forEach(function(m){
1367
+ var el = document.createElement("div");
1368
+ el.className = "cp-msg " + (m.from === "you" ? "you" : m.from === "sys" ? "sys" : "them");
1369
+ // textContent only \u2014 a peer's text is never parsed as HTML.
1370
+ el.textContent = m.text;
1371
+ chatMsgs.appendChild(el);
1372
+ });
1373
+ chatMsgs.scrollTop = chatMsgs.scrollHeight;
1374
+ }
1375
+
1376
+ function openChat(handle){
1377
+ chatWith = handle;
1378
+ delete unread[handle];
1379
+ chatTitle.textContent = handle;
1380
+ chatSub.textContent = peerKnown(handle)
1381
+ ? "live over the P2P link \xB7 text only"
1382
+ : "peer offline \xB7 messages will not deliver";
1383
+ chatPanel.classList.add("is-open");
1384
+ renderChat();
1385
+ renderLiveRows(knownPeers);
1386
+ chatInput.focus();
1387
+ }
1388
+ chatClose.addEventListener("click", function(){
1389
+ chatWith = null;
1390
+ chatPanel.classList.remove("is-open");
1391
+ });
1392
+
1393
+ function sendChat(){
1394
+ var target = chatWith;
1395
+ var text = chatInput.value.trim();
1396
+ if (!target || text === "") return;
1397
+ chatInput.value = "";
1398
+ postChat(target, text).then(function(res){
1399
+ // Honest failure \u2014 never show a message as sent when it wasn't.
1400
+ pushChat(target, res && res.status === 200
1401
+ ? { from: "you", text: text }
1402
+ : { from: "sys", text: "(not delivered \u2014 peer offline?)" });
1403
+ if (chatWith === target) renderChat();
1404
+ }).catch(function(){
1405
+ pushChat(target, { from: "sys", text: "(not delivered \u2014 server unreachable)" });
1406
+ if (chatWith === target) renderChat();
1407
+ });
1408
+ }
1409
+ chatSend.addEventListener("click", sendChat);
1410
+ chatInput.addEventListener("keydown", function(e){ if (e.key === "Enter") sendChat(); });
1411
+
1412
+ // Render the live-peers rows: one per connected peer with its verification
1413
+ // marks, a Chat button (opens the conversation), and a Call button that
1414
+ // rings THAT peer specifically.
1415
+ function renderLiveRows(peers){
1416
+ liveRows.innerHTML = "";
1417
+ peers.forEach(function(p){
1418
+ var row = document.createElement("div");
1419
+ row.className = "live-row";
1420
+ var who = document.createElement("div");
1421
+ // Marks from the peer's hello, shown only when present: \u2713 usage
1422
+ // verified (real local logs), \u{1F511} identity-verified (signed hello).
1423
+ var marks = (p.verified === true ? " \u2713" : "") + (p.identityVerified === true ? " \u{1F511}" : "");
1424
+ var h = document.createElement("div"); h.className = "h"; h.textContent = p.handle + marks;
1425
+ var s = document.createElement("div"); s.className = "s"; s.textContent = p.league + " \xB7 " + p.harness;
1426
+ who.appendChild(h); who.appendChild(s);
1427
+ var actions = document.createElement("div");
1428
+ actions.className = "live-actions";
1429
+ var chatBtn = document.createElement("button");
1430
+ chatBtn.className = "cbtn" + (unread[p.handle] ? " has-unread" : "");
1431
+ chatBtn.type = "button";
1432
+ chatBtn.textContent = "Chat" + (unread[p.handle] ? " (" + unread[p.handle] + ")" : "");
1433
+ chatBtn.addEventListener("click", function(){ openChat(p.handle); });
1434
+ var btn = document.createElement("button");
1435
+ btn.className = "vbtn"; btn.type = "button"; btn.textContent = "Call";
1436
+ btn.addEventListener("click", function(){
1437
+ if (rtc.remoteHandle || pendingOffer) return; // already in a call / prompt
1438
+ btn.disabled = true;
1439
+ // DIRECTED call: the rtc-offer is relayed only to this peer's handle.
1440
+ startCallAsOfferer(p.handle).catch(function(){ hangup(); });
1441
+ });
1442
+ actions.appendChild(chatBtn);
1443
+ actions.appendChild(btn);
1444
+ row.appendChild(who);
1445
+ row.appendChild(actions);
1446
+ liveRows.appendChild(row);
1447
+ });
1448
+ }
1449
+
1156
1450
  async function refreshPeers(){
1157
1451
  try {
1158
1452
  var res = await fetch("/api/live/peers");
@@ -1160,26 +1454,15 @@ var webAppHtml = `<!DOCTYPE html>
1160
1454
  var data = await res.json();
1161
1455
  var peers = (data && data.peers) || [];
1162
1456
  knownPeers = peers;
1457
+ peers.forEach(function(p){ ensureChatLoop(p.handle); });
1458
+ if (chatWith) {
1459
+ chatSub.textContent = peerKnown(chatWith)
1460
+ ? "live over the P2P link \xB7 text only"
1461
+ : "peer offline \xB7 messages will not deliver";
1462
+ }
1163
1463
  if (peers.length === 0) { livePanel.classList.remove("is-open"); return; }
1164
1464
  livePanel.classList.add("is-open");
1165
- liveRows.innerHTML = "";
1166
- peers.forEach(function(p){
1167
- var row = document.createElement("div");
1168
- row.className = "live-row";
1169
- var who = document.createElement("div");
1170
- var h = document.createElement("div"); h.className = "h"; h.textContent = p.handle;
1171
- var s = document.createElement("div"); s.className = "s"; s.textContent = p.league + " \xB7 " + p.harness;
1172
- who.appendChild(h); who.appendChild(s);
1173
- var btn = document.createElement("button");
1174
- btn.className = "vbtn"; btn.type = "button"; btn.textContent = "Video";
1175
- btn.addEventListener("click", function(){
1176
- if (rtc.remoteHandle) return; // already in a call
1177
- btn.disabled = true;
1178
- startCallAsOfferer(p.handle).catch(function(){ btn.disabled = false; });
1179
- });
1180
- row.appendChild(who); row.appendChild(btn);
1181
- liveRows.appendChild(row);
1182
- });
1465
+ renderLiveRows(peers);
1183
1466
  } catch(e){}
1184
1467
  }
1185
1468
  refreshPeers();
@@ -1190,19 +1473,37 @@ var webAppHtml = `<!DOCTYPE html>
1190
1473
  </html>`;
1191
1474
 
1192
1475
  // src/server.ts
1476
+ var MAX_QUEUED_MESSAGES = 200;
1193
1477
  function createLiveBridge() {
1194
1478
  const boxes = /* @__PURE__ */ new Map();
1195
1479
  const bridge = {
1196
1480
  get peers() {
1197
- return [...boxes.values()].map((m) => m.link.hello);
1481
+ return [...boxes.values()].map((m) => {
1482
+ const h = m.link.hello;
1483
+ return {
1484
+ handle: h.handle,
1485
+ league: h.league,
1486
+ harness: h.harness,
1487
+ ...h.verified !== void 0 ? { verified: h.verified } : {},
1488
+ ...h.identityVerified !== void 0 ? { identityVerified: h.identityVerified } : {}
1489
+ };
1490
+ });
1198
1491
  },
1199
1492
  addLink(link) {
1200
1493
  const handle2 = link.hello.handle;
1201
- boxes.set(handle2, { link, incoming: [] });
1494
+ boxes.set(handle2, { link, incoming: [], messages: [] });
1202
1495
  link.onSignal((f) => {
1203
1496
  const mb = boxes.get(handle2);
1204
1497
  if (mb) mb.incoming.push(f);
1205
1498
  });
1499
+ link.onMessage((m) => {
1500
+ const mb = boxes.get(handle2);
1501
+ if (!mb) return;
1502
+ mb.messages.push(m);
1503
+ if (mb.messages.length > MAX_QUEUED_MESSAGES) {
1504
+ mb.messages.splice(0, mb.messages.length - MAX_QUEUED_MESSAGES);
1505
+ }
1506
+ });
1206
1507
  link.onClose(() => {
1207
1508
  const cur = boxes.get(handle2);
1208
1509
  if (cur && cur.link === link) boxes.delete(handle2);
@@ -1223,6 +1524,22 @@ function createLiveBridge() {
1223
1524
  if (cur.incoming.length > 0) return cur.incoming.shift() ?? null;
1224
1525
  }
1225
1526
  return null;
1527
+ },
1528
+ sendMessage(handle2, text) {
1529
+ boxes.get(handle2)?.link.send(text);
1530
+ },
1531
+ async pollMessage(handle2, timeoutMs) {
1532
+ const mb = boxes.get(handle2);
1533
+ if (!mb) return null;
1534
+ if (mb.messages.length > 0) return mb.messages.shift() ?? null;
1535
+ const deadline = Date.now() + timeoutMs;
1536
+ while (Date.now() < deadline) {
1537
+ await new Promise((r) => setTimeout(r, 100));
1538
+ const cur = boxes.get(handle2);
1539
+ if (!cur) return null;
1540
+ if (cur.messages.length > 0) return cur.messages.shift() ?? null;
1541
+ }
1542
+ return null;
1226
1543
  }
1227
1544
  };
1228
1545
  return bridge;
@@ -1396,11 +1713,62 @@ async function handle(req, res, opts) {
1396
1713
  sendJson(res, 200, { ok: true });
1397
1714
  return;
1398
1715
  }
1716
+ if (req.method === "GET" && pathname === "/live/message") {
1717
+ const live = opts.live;
1718
+ if (!live) {
1719
+ sendJson(res, 200, { message: null, reason: "live-not-attached" });
1720
+ return;
1721
+ }
1722
+ const handle2 = url.searchParams.get("handle") ?? "";
1723
+ if (handle2 === "") {
1724
+ sendJson(res, 400, { error: "missing handle" });
1725
+ return;
1726
+ }
1727
+ const message = await live.pollMessage(handle2, 25e3);
1728
+ if (req.destroyed || res.writableEnded) return;
1729
+ sendJson(res, 200, { message });
1730
+ return;
1731
+ }
1732
+ if (req.method === "POST" && pathname === "/live/message") {
1733
+ const live = opts.live;
1734
+ if (!live) {
1735
+ sendJson(res, 400, { error: "live-not-attached" });
1736
+ return;
1737
+ }
1738
+ const body = await readBody(req);
1739
+ let parsed = {};
1740
+ try {
1741
+ parsed = JSON.parse(body);
1742
+ } catch {
1743
+ sendJson(res, 400, { error: "invalid JSON body" });
1744
+ return;
1745
+ }
1746
+ const handle2 = typeof parsed["handle"] === "string" ? parsed["handle"] : "";
1747
+ if (handle2 === "") {
1748
+ sendJson(res, 400, { error: "missing handle" });
1749
+ return;
1750
+ }
1751
+ const text = parsed["text"];
1752
+ if (typeof text !== "string") {
1753
+ sendJson(res, 400, { error: "missing text" });
1754
+ return;
1755
+ }
1756
+ const reParsed = parseFrame(
1757
+ JSON.stringify({ t: "msg", id: randomUUID(), text, at: Date.now() })
1758
+ );
1759
+ if (reParsed === null || reParsed.t !== "msg") {
1760
+ sendJson(res, 400, { error: "invalid message text" });
1761
+ return;
1762
+ }
1763
+ live.sendMessage(handle2, reParsed.text);
1764
+ sendJson(res, 200, { ok: true });
1765
+ return;
1766
+ }
1399
1767
  sendJson(res, 404, { error: "not found" });
1400
1768
  }
1401
1769
 
1402
1770
  // src/cli.ts
1403
- var VERSION = "0.4.0";
1771
+ var VERSION = "0.4.1";
1404
1772
  function parsePort(raw) {
1405
1773
  const n = Number(raw);
1406
1774
  if (!Number.isInteger(n) || n < 1 || n > 65535) return void 0;
@@ -1698,7 +2066,7 @@ async function cmdDiscover(live, any) {
1698
2066
  );
1699
2067
  return 0;
1700
2068
  }
1701
- async function cmdOpen(port) {
2069
+ async function cmdOpen(port, any) {
1702
2070
  const profile = loadProfile();
1703
2071
  let live;
1704
2072
  let session;
@@ -1712,7 +2080,14 @@ async function cmdOpen(port) {
1712
2080
  ${LIVE_NOTICE}
1713
2081
  `);
1714
2082
  const hello = buildHello(profile);
1715
- void startDiscovery({ hello, isBlocked: blockedChecker(), onLink: (link) => live.addLink(link) }).then((s) => {
2083
+ const { topics, acceptLeague } = discoveryScope(profile.league, any);
2084
+ void startDiscovery({
2085
+ hello,
2086
+ topics,
2087
+ acceptLeague,
2088
+ isBlocked: blockedChecker(),
2089
+ onLink: (link) => live.addLink(link)
2090
+ }).then((s) => {
1716
2091
  session = s;
1717
2092
  }).catch(() => {
1718
2093
  });
@@ -1721,9 +2096,11 @@ async function cmdOpen(port) {
1721
2096
  vibedating local web app \u2192 ${started.url}
1722
2097
  `);
1723
2098
  if (live) {
1724
- process2.stdout.write(" \u2022 live A/V (video) available for connected same-league peers\n");
2099
+ process2.stdout.write(
2100
+ any ? " \u2022 live video + chat available for connected peers (ANY league \u2014 --any)\n" : " \u2022 live video + chat available for connected peers (your league + adjacent; --any = everyone)\n"
2101
+ );
1725
2102
  } else {
1726
- process2.stdout.write(" \u2022 connect first (`vibedating connect`) to enable live video\n");
2103
+ process2.stdout.write(" \u2022 connect first (`vibedating connect`) to enable live video + chat\n");
1727
2104
  }
1728
2105
  process2.stdout.write(" \u2022 raw usage stays local \xB7 only league shared\n");
1729
2106
  process2.stdout.write(" (Ctrl+C to stop)\n\n");
@@ -1986,8 +2363,9 @@ Usage:
1986
2363
  vibedating block <@handle> Block a handle \u2014 their hello is dropped (never recorded/paired)
1987
2364
  vibedating unblock <@handle> Remove a handle from the blocklist
1988
2365
  vibedating blocklist List blocked handles
1989
- vibedating open [--port N] Serve the local web app (default: random port)
1990
- + live A/V video with connected same-league peers
2366
+ vibedating open [--port N] [--any] Serve the local web app (default: random port)
2367
+ + live video + chat with connected peers
2368
+ (your league + adjacent; --any = every league)
1991
2369
  vibedating mcp Run the stdio MCP server (profile, matches)
1992
2370
  vibedating --version
1993
2371
  vibedating --help
@@ -2001,9 +2379,10 @@ Privacy:
2001
2379
  (real local logs) \xB7 ~ unverified \xB7 \u{1F511} identity-verified (signed hello).
2002
2380
 
2003
2381
  Matching:
2004
- discover/live match your league + adjacent (\xB11) tiers by default, so thin
2005
- leagues and cross-league friends still connect. --any matches every league.
2006
- find / live --to look for one specific handle instead of the first random peer.
2382
+ discover/live/open match your league + adjacent (\xB11) tiers by default, so
2383
+ thin leagues and cross-league friends still connect. --any matches every
2384
+ league. find / live --to look for one specific handle instead of the first
2385
+ random peer.
2007
2386
 
2008
2387
  Env:
2009
2388
  VIBEDATING_TOKENS=<n> Self-report a token count (e.g. 23400000 or 12M)
@@ -2036,7 +2415,7 @@ async function main(argv) {
2036
2415
  case "discover":
2037
2416
  return cmdDiscover(parsed.live, parsed.any);
2038
2417
  case "open":
2039
- return cmdOpen(parsed.port);
2418
+ return cmdOpen(parsed.port, parsed.any);
2040
2419
  case "live":
2041
2420
  return cmdLive(parsed.dating, parsed.any, parsed.to);
2042
2421
  case "find":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibedate",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Dating by tokens — matched by usage league across agentic CLIs. Raw usage stays local; only your league is shared. CLI + local web app + MCP. Local-first.",
5
5
  "type": "module",
6
6
  "license": "MIT",