vibedate 0.7.0 → 0.7.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.
Files changed (2) hide show
  1. package/dist/cli.js +331 -35
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -914,6 +914,56 @@ var webAppHtml = `<!DOCTYPE html>
914
914
  .incoming-call p{ color: var(--muted); font-size: .85rem; margin: 0 0 18px; }
915
915
  .incoming-call .ic-actions{ display: flex; gap: 10px; }
916
916
 
917
+ /* ---- room full-mesh group video ---- */
918
+ .room-panel{
919
+ position: fixed; right: 18px; bottom: 18px; z-index: 45;
920
+ background: linear-gradient(180deg, var(--bg-card), var(--bg-card-2));
921
+ border: 1px solid var(--border-2); border-radius: 16px; box-shadow: var(--shadow-2);
922
+ padding: 12px 14px; min-width: 240px; max-width: 280px; display: none;
923
+ }
924
+ .room-panel.is-open{ display: block; animation: rise var(--dur-med) var(--ease-out); }
925
+ .room-panel .rp-title{ font-size: .78rem; font-weight: 800; margin-bottom: 6px; display:flex; align-items:center; gap:6px; }
926
+ .room-panel .rp-meta{ font-size: .7rem; color: var(--muted-2); margin-bottom: 10px; line-height: 1.4; word-break: break-all; }
927
+ .room-panel .rp-actions{ display:flex; gap: 8px; }
928
+ .room-panel .rp-btn{
929
+ border: 0; border-radius: 9px; padding: 8px 14px; font-weight: 700; font-size: .78rem;
930
+ background: linear-gradient(180deg, var(--coral), var(--coral-dim)); color: #2a1109;
931
+ flex: 1;
932
+ }
933
+ .room-panel .rp-btn:disabled{ opacity: .4; cursor: not-allowed; }
934
+ .room-panel .rp-btn.is-leave{ background: var(--danger); color: #2a0a0c; }
935
+ .room-video{
936
+ position: fixed; inset: 0; z-index: 68;
937
+ display: none; flex-direction: column; align-items: stretch; justify-content: flex-start;
938
+ background: rgba(12,7,15,.86); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
939
+ padding: 18px 18px 80px;
940
+ }
941
+ .room-video.is-open{ display: flex; }
942
+ .room-video .rv-head{
943
+ display:flex; align-items:center; justify-content: space-between; gap: 12px;
944
+ margin-bottom: 14px; color: var(--fg); font-weight: 800; font-size: .9rem;
945
+ }
946
+ .room-video .rv-grid{
947
+ flex: 1; display: grid; gap: 12px; min-height: 0;
948
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
949
+ align-content: start;
950
+ }
951
+ .room-video .rv-tile{
952
+ display:flex; flex-direction: column; gap: 6px; min-width: 0;
953
+ }
954
+ .room-video .rv-tile video{
955
+ width: 100%; aspect-ratio: 4 / 3; background: #000; border-radius: 14px;
956
+ border: 1px solid var(--border-2); object-fit: cover;
957
+ }
958
+ .room-video .rv-tile .rv-label{
959
+ font-size: .75rem; color: var(--muted); font-weight: 600; word-break: break-all;
960
+ }
961
+ .room-video .rv-leave{
962
+ position: absolute; bottom: 24px; left: 50%; transform: translateX(-50%);
963
+ border: 0; border-radius: 999px; padding: 12px 22px; font-weight: 800; font-size: .9rem;
964
+ background: var(--danger); color: #2a0a0c;
965
+ }
966
+
917
967
  /* ---- live text chat (browser <-> PeerLink over /live/message) ---- */
918
968
  .live-actions{ display: flex; gap: 6px; flex-shrink: 0; }
919
969
  .live-row .cbtn{
@@ -1151,6 +1201,24 @@ var webAppHtml = `<!DOCTYPE html>
1151
1201
  <button class="vhangup" id="hangupBtn" type="button">Hang up</button>
1152
1202
  </div>
1153
1203
 
1204
+ <aside class="room-panel" id="roomPanel" aria-label="Room">
1205
+ <div class="rp-title"><span class="lp-dot" aria-hidden="true"></span> Room</div>
1206
+ <div class="rp-meta" id="roomMeta">not in a room</div>
1207
+ <div class="rp-actions">
1208
+ <button class="rp-btn" id="roomJoinBtn" type="button">Join video</button>
1209
+ <button class="rp-btn is-leave" id="roomLeaveBtn" type="button" disabled>Leave</button>
1210
+ </div>
1211
+ </aside>
1212
+
1213
+ <div class="room-video" id="roomVideo" role="dialog" aria-modal="true" aria-label="Room video">
1214
+ <div class="rv-head">
1215
+ <span id="roomVideoTitle">Room video</span>
1216
+ <span id="roomVideoCount" style="font-weight:600;color:var(--muted);font-size:.78rem"></span>
1217
+ </div>
1218
+ <div class="rv-grid" id="roomVideoGrid"></div>
1219
+ <button class="rv-leave" id="roomVideoLeaveBtn" type="button">Leave video</button>
1220
+ </div>
1221
+
1154
1222
  <script>
1155
1223
  (function(){
1156
1224
  "use strict";
@@ -1816,6 +1884,252 @@ var webAppHtml = `<!DOCTYPE html>
1816
1884
  }
1817
1885
  refreshPeers();
1818
1886
  setInterval(refreshPeers, 4000);
1887
+
1888
+ /* ---- Room full-mesh group video ----------------------------------------
1889
+ * When vibedate open --room <name> is attached, /api/room exposes the
1890
+ * roster. Each browser grabs getUserMedia once and opens one
1891
+ * RTCPeerConnection to every other member. Deterministic offerer election
1892
+ * (self < other lexicographic) avoids glare. Signaling rides the local
1893
+ * server's merged mailbox: GET /room/signal?handle=SELF returns
1894
+ * { frame: { from, rtc } }, POST /room/signal { handle: TARGET, frame }
1895
+ * relays one rtc-* frame to that member. */
1896
+ var roomPanel = document.getElementById("roomPanel");
1897
+ var roomMeta = document.getElementById("roomMeta");
1898
+ var roomJoinBtn = document.getElementById("roomJoinBtn");
1899
+ var roomLeaveBtn = document.getElementById("roomLeaveBtn");
1900
+ var roomVideo = document.getElementById("roomVideo");
1901
+ var roomVideoGrid = document.getElementById("roomVideoGrid");
1902
+ var roomVideoTitle = document.getElementById("roomVideoTitle");
1903
+ var roomVideoCount = document.getElementById("roomVideoCount");
1904
+ var roomVideoLeaveBtn = document.getElementById("roomVideoLeaveBtn");
1905
+
1906
+ var roomVid = { localStream: null, peers: {}, self: null, room: null, joined: false, polling: false };
1907
+ var roomLastRoster = [];
1908
+
1909
+ function roomPostSignal(handle, frame){
1910
+ return fetch("/room/signal", {
1911
+ method: "POST",
1912
+ headers: { "content-type": "application/json" },
1913
+ body: JSON.stringify({ handle: handle, frame: frame })
1914
+ }).catch(function(){ /* best effort */ });
1915
+ }
1916
+
1917
+ function roomFetchSignal(selfHandle, timeoutMs){
1918
+ var ctrl = new AbortController();
1919
+ var t = setTimeout(function(){ ctrl.abort(); }, timeoutMs);
1920
+ return fetch("/room/signal?handle=" + encodeURIComponent(selfHandle), { signal: ctrl.signal })
1921
+ .then(function(r){ clearTimeout(t); return r; })
1922
+ .catch(function(e){ clearTimeout(t); throw e; });
1923
+ }
1924
+
1925
+ function makeRoomTile(label, muted){
1926
+ var tile = document.createElement("div");
1927
+ tile.className = "rv-tile";
1928
+ var vid = document.createElement("video");
1929
+ vid.autoplay = true;
1930
+ vid.playsInline = true;
1931
+ if (muted) vid.muted = true;
1932
+ var lab = document.createElement("div");
1933
+ lab.className = "rv-label";
1934
+ lab.textContent = label;
1935
+ tile.appendChild(vid);
1936
+ tile.appendChild(lab);
1937
+ roomVideoGrid.appendChild(tile);
1938
+ return vid;
1939
+ }
1940
+
1941
+ function updateRoomCount(){
1942
+ var n = 0;
1943
+ if (roomVid.localStream) n++;
1944
+ for (var k in roomVid.peers) if (Object.prototype.hasOwnProperty.call(roomVid.peers, k)) n++;
1945
+ roomVideoCount.textContent = n ? (n + " tile" + (n === 1 ? "" : "s")) : "";
1946
+ }
1947
+
1948
+ function connectPeer(handle, role){
1949
+ if (!roomVid.localStream || !roomVid.self) return null;
1950
+ if (roomVid.peers[handle]) return roomVid.peers[handle];
1951
+ var pc = new RTCPeerConnection(rtcConfig());
1952
+ var videoEl = makeRoomTile(handle, false);
1953
+ var peer = { pc: pc, videoEl: videoEl, role: role };
1954
+ roomVid.peers[handle] = peer;
1955
+ updateRoomCount();
1956
+
1957
+ roomVid.localStream.getTracks().forEach(function(t){ pc.addTrack(t, roomVid.localStream); });
1958
+ pc.onicecandidate = function(e){
1959
+ // Trickle ICE to this peer. Empty candidate = end-of-candidates marker.
1960
+ roomPostSignal(handle, {
1961
+ t: "rtc-ice",
1962
+ candidate: e.candidate && e.candidate.candidate ? e.candidate.candidate : ""
1963
+ });
1964
+ };
1965
+ pc.ontrack = function(e){
1966
+ if (e.streams && e.streams[0]) videoEl.srcObject = e.streams[0];
1967
+ };
1968
+ pc.onconnectionstatechange = function(){
1969
+ // Quiet \u2014 closed peers stay in the grid until Leave clears everything.
1970
+ };
1971
+
1972
+ if (role === "offerer") {
1973
+ (async function(){
1974
+ try {
1975
+ var offer = await pc.createOffer();
1976
+ await pc.setLocalDescription(offer);
1977
+ await roomPostSignal(handle, { t: "rtc-offer", sdp: offer.sdp });
1978
+ } catch(e){ /* offer failed \u2014 peer will retry on next roster refresh if still present */ }
1979
+ })();
1980
+ }
1981
+ return peer;
1982
+ }
1983
+
1984
+ async function handleRoomSignal(from, rtc){
1985
+ if (!rtc || !from || from === roomVid.self) return;
1986
+ if (rtc.t === "rtc-offer") {
1987
+ var peer = roomVid.peers[from];
1988
+ if (!peer) peer = connectPeer(from, "answerer");
1989
+ if (!peer) return;
1990
+ try {
1991
+ await peer.pc.setRemoteDescription({ type: "offer", sdp: rtc.sdp });
1992
+ var answer = await peer.pc.createAnswer();
1993
+ await peer.pc.setLocalDescription(answer);
1994
+ await roomPostSignal(from, { t: "rtc-answer", sdp: answer.sdp });
1995
+ } catch(e){}
1996
+ return;
1997
+ }
1998
+ if (rtc.t === "rtc-answer") {
1999
+ var pAns = roomVid.peers[from];
2000
+ if (!pAns) return;
2001
+ try { await pAns.pc.setRemoteDescription({ type: "answer", sdp: rtc.sdp }); } catch(e){}
2002
+ return;
2003
+ }
2004
+ if (rtc.t === "rtc-ice") {
2005
+ var pIce = roomVid.peers[from];
2006
+ if (!pIce) return;
2007
+ // Skip empty candidate (end-of-candidates) \u2014 browsers tolerate either way.
2008
+ if (!rtc.candidate) return;
2009
+ try { await pIce.pc.addIceCandidate({ candidate: rtc.candidate }); } catch(e){}
2010
+ }
2011
+ }
2012
+
2013
+ async function roomSignalLoop(){
2014
+ if (roomVid.polling) return;
2015
+ roomVid.polling = true;
2016
+ while (roomVid.joined && roomVid.self) {
2017
+ var res;
2018
+ try { res = await roomFetchSignal(roomVid.self, 5000); }
2019
+ catch(e){ await sleep(400); continue; }
2020
+ if (!res || res.status !== 200) { await sleep(400); continue; }
2021
+ var data;
2022
+ try { data = await res.json(); } catch(e){ continue; }
2023
+ var f = data && data.frame;
2024
+ if (!f) continue; // timeout empty
2025
+ // Sender-routed shape: { from, rtc } (or a nested frame.frame fallback).
2026
+ var from = f.from;
2027
+ var rtc = f.rtc || f.frame || f;
2028
+ if (from && rtc && rtc.t) await handleRoomSignal(from, rtc);
2029
+ }
2030
+ roomVid.polling = false;
2031
+ }
2032
+
2033
+ function meshWithMembers(members){
2034
+ if (!roomVid.joined || !roomVid.self || !roomVid.localStream) return;
2035
+ var self = roomVid.self;
2036
+ var seen = {};
2037
+ (members || []).forEach(function(m){
2038
+ var h = m && m.handle;
2039
+ if (!h || h === self) return;
2040
+ seen[h] = true;
2041
+ if (roomVid.peers[h]) return;
2042
+ // Deterministic glare avoidance: lower handle is the offerer.
2043
+ if (self < h) connectPeer(h, "offerer");
2044
+ // else wait for their offer via the long-poll loop
2045
+ });
2046
+ }
2047
+
2048
+ async function joinRoomVideo(){
2049
+ if (roomVid.joined) return;
2050
+ if (!roomVid.self || !roomVid.room) return;
2051
+ roomJoinBtn.disabled = true;
2052
+ try {
2053
+ roomVid.localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
2054
+ } catch(e){
2055
+ roomJoinBtn.disabled = false;
2056
+ roomMeta.textContent = (roomVid.room || "room") + " \xB7 camera/mic blocked";
2057
+ return;
2058
+ }
2059
+ roomVid.joined = true;
2060
+ roomVid.peers = {};
2061
+ roomVideoGrid.innerHTML = "";
2062
+ var localEl = makeRoomTile((roomVid.self || "you") + " (you)", true);
2063
+ localEl.srcObject = roomVid.localStream;
2064
+ roomVideo.classList.add("is-open");
2065
+ roomVideoTitle.textContent = "Room \xB7 " + roomVid.room;
2066
+ roomLeaveBtn.disabled = false;
2067
+ updateRoomCount();
2068
+ roomSignalLoop();
2069
+ // Seed mesh against the latest roster, then refresh once more for races.
2070
+ meshWithMembers(roomLastRoster);
2071
+ try {
2072
+ var res = await fetch("/api/room");
2073
+ if (res.status === 200) {
2074
+ var data = await res.json();
2075
+ var members = (data && data.members) || [];
2076
+ roomLastRoster = members;
2077
+ meshWithMembers(members);
2078
+ }
2079
+ } catch(e){}
2080
+ }
2081
+
2082
+ function leaveRoomVideo(){
2083
+ roomVid.joined = false;
2084
+ for (var h in roomVid.peers) {
2085
+ if (!Object.prototype.hasOwnProperty.call(roomVid.peers, h)) continue;
2086
+ try { roomVid.peers[h].pc.close(); } catch(e){}
2087
+ }
2088
+ roomVid.peers = {};
2089
+ if (roomVid.localStream) {
2090
+ roomVid.localStream.getTracks().forEach(function(t){ try { t.stop(); } catch(e){} });
2091
+ roomVid.localStream = null;
2092
+ }
2093
+ roomVideoGrid.innerHTML = "";
2094
+ roomVideo.classList.remove("is-open");
2095
+ roomLeaveBtn.disabled = true;
2096
+ roomJoinBtn.disabled = !roomVid.room;
2097
+ updateRoomCount();
2098
+ }
2099
+
2100
+ roomJoinBtn.addEventListener("click", function(){ joinRoomVideo().catch(function(){ leaveRoomVideo(); }); });
2101
+ roomLeaveBtn.addEventListener("click", leaveRoomVideo);
2102
+ roomVideoLeaveBtn.addEventListener("click", leaveRoomVideo);
2103
+
2104
+ async function refreshRoom(){
2105
+ try {
2106
+ var res = await fetch("/api/room");
2107
+ if (res.status !== 200) return;
2108
+ var data = await res.json();
2109
+ var roomName = data && data.room;
2110
+ if (!roomName) {
2111
+ if (roomVid.joined) leaveRoomVideo();
2112
+ roomVid.room = null;
2113
+ roomVid.self = null;
2114
+ roomLastRoster = [];
2115
+ roomPanel.classList.remove("is-open");
2116
+ return;
2117
+ }
2118
+ roomVid.room = roomName;
2119
+ roomVid.self = data.self || null;
2120
+ roomLastRoster = (data.members) || [];
2121
+ var n = roomLastRoster.length;
2122
+ roomMeta.textContent = roomName +
2123
+ (roomVid.self ? " \xB7 you " + roomVid.self : "") +
2124
+ " \xB7 " + n + " other" + (n === 1 ? "" : "s");
2125
+ roomPanel.classList.add("is-open");
2126
+ roomJoinBtn.disabled = roomVid.joined || !roomVid.self;
2127
+ roomLeaveBtn.disabled = !roomVid.joined;
2128
+ if (roomVid.joined) meshWithMembers(roomLastRoster);
2129
+ } catch(e){}
2130
+ }
2131
+ refreshRoom();
2132
+ setInterval(refreshRoom, 4000);
1819
2133
  })();
1820
2134
  </script>
1821
2135
  </body>
@@ -1898,8 +2212,8 @@ function createRoomBridge(name) {
1898
2212
  let session;
1899
2213
  const messageQueue = [];
1900
2214
  const messageWaiters = [];
1901
- const signalBoxes = /* @__PURE__ */ new Map();
1902
- const signalWaiters = /* @__PURE__ */ new Map();
2215
+ const signalQueue = [];
2216
+ const signalWaiters = [];
1903
2217
  const drainMessage = (m) => {
1904
2218
  const safe = { ...m, text: sanitizePeerText(m.text) };
1905
2219
  const waiter = messageWaiters.shift();
@@ -1913,23 +2227,15 @@ function createRoomBridge(name) {
1913
2227
  }
1914
2228
  };
1915
2229
  const drainSignal = (from, frame) => {
1916
- const waiters = signalWaiters.get(from);
1917
- if (waiters !== void 0 && waiters.length > 0) {
1918
- const w = waiters.shift();
1919
- if (w !== void 0) {
1920
- if (waiters.length === 0) signalWaiters.delete(from);
1921
- w(frame);
1922
- return;
1923
- }
1924
- }
1925
- let box = signalBoxes.get(from);
1926
- if (box === void 0) {
1927
- box = [];
1928
- signalBoxes.set(from, box);
2230
+ const tagged = { from, rtc: frame };
2231
+ const waiter = signalWaiters.shift();
2232
+ if (waiter !== void 0) {
2233
+ waiter(tagged);
2234
+ return;
1929
2235
  }
1930
- box.push(frame);
1931
- if (box.length > MAX_QUEUED_ROOM_SIGNALS) {
1932
- box.splice(0, box.length - MAX_QUEUED_ROOM_SIGNALS);
2236
+ signalQueue.push(tagged);
2237
+ if (signalQueue.length > MAX_QUEUED_ROOM_SIGNALS) {
2238
+ signalQueue.splice(0, signalQueue.length - MAX_QUEUED_ROOM_SIGNALS);
1933
2239
  }
1934
2240
  };
1935
2241
  return {
@@ -1972,27 +2278,17 @@ function createRoomBridge(name) {
1972
2278
  sendSignal(handle2, frame) {
1973
2279
  session?.sendSignal(handle2, frame);
1974
2280
  },
1975
- async pollSignal(handle2, timeoutMs) {
1976
- const box = signalBoxes.get(handle2);
1977
- if (box !== void 0 && box.length > 0) return box.shift() ?? null;
2281
+ async pollSignal(_handle, timeoutMs) {
2282
+ if (signalQueue.length > 0) return signalQueue.shift() ?? null;
1978
2283
  return new Promise((resolve) => {
1979
2284
  const timer = setTimeout(() => {
1980
- const waiters2 = signalWaiters.get(handle2);
1981
- if (waiters2 !== void 0) {
1982
- const idx = waiters2.indexOf(resolve);
1983
- if (idx >= 0) waiters2.splice(idx, 1);
1984
- if (waiters2.length === 0) signalWaiters.delete(handle2);
1985
- }
2285
+ const idx = signalWaiters.indexOf(resolve);
2286
+ if (idx >= 0) signalWaiters.splice(idx, 1);
1986
2287
  resolve(null);
1987
2288
  }, timeoutMs);
1988
- let waiters = signalWaiters.get(handle2);
1989
- if (waiters === void 0) {
1990
- waiters = [];
1991
- signalWaiters.set(handle2, waiters);
1992
- }
1993
- waiters.push((f) => {
2289
+ signalWaiters.push((s) => {
1994
2290
  clearTimeout(timer);
1995
- resolve(f);
2291
+ resolve(s);
1996
2292
  });
1997
2293
  });
1998
2294
  }
@@ -2312,7 +2608,7 @@ async function handle(req, res, opts) {
2312
2608
  }
2313
2609
 
2314
2610
  // src/cli.ts
2315
- var VERSION = "0.7.0";
2611
+ var VERSION = "0.7.1";
2316
2612
  function parsePort(raw) {
2317
2613
  const n = Number(raw);
2318
2614
  if (!Number.isInteger(n) || n < 1 || n > 65535) return void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibedate",
3
- "version": "0.7.0",
3
+ "version": "0.7.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",