vibedate 0.8.1 → 0.8.3

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.
@@ -778,11 +778,13 @@ function parseHandshake(raw) {
778
778
  function peersPath(dir) {
779
779
  return path4.join(dir, "peers.json");
780
780
  }
781
- function loadPeers(dir = defaultStateDir()) {
781
+ function loadPeers(dir = defaultStateDir(), maxAgeMs = 7 * 24 * 60 * 60 * 1e3) {
782
782
  try {
783
783
  const raw = readFileSync3(peersPath(dir), "utf8");
784
784
  const data = JSON.parse(raw);
785
- return Array.isArray(data.peers) ? data.peers : [];
785
+ if (!Array.isArray(data.peers)) return [];
786
+ const now = Date.now();
787
+ return data.peers.filter((p) => now - new Date(p.lastSeenAt).getTime() <= maxAgeMs);
786
788
  } catch {
787
789
  return [];
788
790
  }
@@ -798,7 +800,9 @@ function recordPeer(hello, dir = defaultStateDir(), now = /* @__PURE__ */ new Da
798
800
  ...hello.pubkey !== void 0 ? { pubkey: hello.pubkey } : {},
799
801
  ...hello.identityVerified !== void 0 ? { identityVerified: hello.identityVerified } : {}
800
802
  };
801
- const existing = peers.findIndex((p) => p.handle === clean.handle);
803
+ const existing = peers.findIndex(
804
+ (p) => clean.pubkey !== void 0 && p.pubkey !== void 0 ? p.pubkey === clean.pubkey : p.handle === clean.handle
805
+ );
802
806
  let isNew;
803
807
  let peer;
804
808
  if (existing >= 0) {
@@ -821,10 +825,12 @@ function recordPeer(hello, dir = defaultStateDir(), now = /* @__PURE__ */ new Da
821
825
  writeFileSync4(peersPath(dir), JSON.stringify({ peers }, null, 2) + "\n", "utf8");
822
826
  return { peer, isNew };
823
827
  }
824
- function recordPeerMessage(handle, dir = defaultStateDir(), now = /* @__PURE__ */ new Date()) {
828
+ function recordPeerMessage(peer, dir = defaultStateDir(), now = /* @__PURE__ */ new Date()) {
825
829
  try {
826
830
  const peers = loadPeers(dir);
827
- const idx = peers.findIndex((p) => p.handle === handle);
831
+ const idx = peers.findIndex(
832
+ (p) => peer.pubkey !== void 0 && p.pubkey !== void 0 ? p.pubkey === peer.pubkey : p.handle === peer.handle
833
+ );
828
834
  if (idx < 0) return false;
829
835
  peers[idx] = { ...peers[idx], lastMessageAt: now.toISOString() };
830
836
  mkdirSync3(dir, { recursive: true });
@@ -907,7 +913,7 @@ async function startDiscovery(opts) {
907
913
  if (onLink !== void 0) {
908
914
  const link = createPeerLink(socket, peer, buf);
909
915
  link.onMessage(() => {
910
- recordPeerMessage(peer.handle, stateDir);
916
+ recordPeerMessage(peer, stateDir);
911
917
  });
912
918
  onLink(link);
913
919
  }
@@ -29,7 +29,7 @@ import {
29
29
  signHelloClaims,
30
30
  startDiscovery,
31
31
  startRoom
32
- } from "./chunk-S7MO5OSO.js";
32
+ } from "./chunk-3FJDVN72.js";
33
33
 
34
34
  // src/mcp.ts
35
35
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -14959,6 +14959,21 @@ function createPairing() {
14959
14959
  return current;
14960
14960
  },
14961
14961
  add(link) {
14962
+ const isSamePeer = (a, b) => {
14963
+ if (a.hello.pubkey !== void 0 && b.hello.pubkey !== void 0) {
14964
+ return a.hello.pubkey === b.hello.pubkey;
14965
+ }
14966
+ return a.hello.handle === b.hello.handle;
14967
+ };
14968
+ const existingIdx = queue.findIndex((l) => isSamePeer(l, link));
14969
+ if (existingIdx >= 0) {
14970
+ const old = queue.splice(existingIdx, 1)[0];
14971
+ old.close();
14972
+ } else if (current !== void 0 && isSamePeer(current, link)) {
14973
+ const old = current;
14974
+ current = void 0;
14975
+ old.close();
14976
+ }
14962
14977
  watch(link);
14963
14978
  bindMessages(link);
14964
14979
  if (current === void 0) {
package/dist/cli.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  stopDaemon,
11
11
  uninstallDaemonService,
12
12
  writeDaemonState
13
- } from "./chunk-JBADPOR5.js";
13
+ } from "./chunk-XUWNLMOY.js";
14
14
  import {
15
15
  CANDIDATES,
16
16
  LIVE_NOTICE,
@@ -46,7 +46,7 @@ import {
46
46
  signHelloClaims,
47
47
  startDiscovery,
48
48
  startRoom
49
- } from "./chunk-S7MO5OSO.js";
49
+ } from "./chunk-3FJDVN72.js";
50
50
 
51
51
  // src/cli.ts
52
52
  import readline from "readline";
@@ -626,6 +626,187 @@ var webAppHtml = `<!DOCTYPE html>
626
626
  }
627
627
  .chat-panel .cp-send:hover{ filter: brightness(1.06); }
628
628
  .chat-panel .cp-send:active{ transform: scale(.95); }
629
+
630
+ /* ---- you identity chip (header) ---- */
631
+ .you-chip{
632
+ display:none; align-items:center; gap:10px;
633
+ padding: 6px 12px 6px 7px; border-radius: 999px;
634
+ background: rgba(0,0,0,.28); border: 1px solid var(--border-2);
635
+ max-width: min(420px, 52vw); min-width: 0;
636
+ }
637
+ .you-chip.is-on{ display:inline-flex; }
638
+ .you-chip .yc-avatar{
639
+ width:30px; height:30px; border-radius:50%; overflow:hidden; flex-shrink:0;
640
+ border: 1px solid var(--border-2); background: var(--bg-1);
641
+ }
642
+ .you-chip .yc-avatar svg{ width:100%; height:100%; display:block; }
643
+ .you-chip .yc-meta{ min-width:0; display:flex; flex-direction:column; gap:1px; }
644
+ .you-chip .yc-handle{
645
+ font-size:.78rem; font-weight:700; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
646
+ }
647
+ .you-chip .yc-sub{
648
+ font-size:.68rem; color: var(--muted-2); display:flex; align-items:center; gap:6px; flex-wrap:wrap;
649
+ }
650
+ .topbar-right{ display:flex; align-items:center; gap:10px; flex-wrap:wrap; justify-content:flex-end; }
651
+
652
+ /* ---- identity / status marks ---- */
653
+ .mark{
654
+ display:inline-flex; align-items:center; gap:3px;
655
+ font-size:.66rem; font-weight:700; letter-spacing:.01em;
656
+ padding: 2px 7px; border-radius:999px; line-height:1.3; white-space:nowrap;
657
+ }
658
+ .mark-verified{
659
+ color: #bff2df; background: rgba(127,227,192,.12); border: 1px solid rgba(127,227,192,.32);
660
+ }
661
+ .mark-identity{
662
+ color: #e8d9ff; background: rgba(203,168,255,.12); border: 1px solid rgba(203,168,255,.32);
663
+ }
664
+ .mark-demo{
665
+ color: var(--muted-2); background: rgba(248,239,232,.05); border: 1px solid var(--border);
666
+ text-transform: uppercase; letter-spacing: .06em;
667
+ }
668
+ .mark-live{
669
+ color: #ffd0c8; background: rgba(255,122,104,.12); border: 1px solid rgba(255,122,104,.35);
670
+ }
671
+ .lg-pill{
672
+ display:inline-flex; align-items:center; gap:5px;
673
+ font-size:.7rem; font-weight:700; padding: 3px 9px; border-radius:999px;
674
+ }
675
+
676
+ /* ---- people column (live + demo) ---- */
677
+ .people-col{ display:flex; flex-direction:column; gap: 22px; min-width: 0; }
678
+ .section-head{
679
+ display:flex; align-items:baseline; justify-content:space-between; gap:10px;
680
+ margin: 0 0 12px;
681
+ }
682
+ .section-head h2{
683
+ font-size: 1.02rem; font-weight: 700; margin:0;
684
+ display:flex; align-items:center; gap:8px;
685
+ }
686
+ .section-head .count{
687
+ font-size:.72rem; font-weight:700; color: var(--muted-2);
688
+ background: rgba(0,0,0,.22); border: 1px solid var(--border);
689
+ padding: 3px 8px; border-radius:999px;
690
+ }
691
+ .section-sub{
692
+ font-size:.78rem; color: var(--muted-2); margin: -6px 0 12px; line-height:1.45;
693
+ }
694
+
695
+ /* Live now roster cards */
696
+ .live-roster{ display:flex; flex-direction:column; gap:10px; }
697
+ .live-card{
698
+ display:flex; align-items:center; gap:12px;
699
+ background: linear-gradient(165deg, rgba(255,122,104,.08), transparent 55%),
700
+ linear-gradient(180deg, var(--bg-card), var(--bg-card-2));
701
+ border: 1px solid rgba(255,122,104,.22);
702
+ border-radius: 16px; padding: 14px 14px;
703
+ box-shadow: var(--shadow-1);
704
+ transition: border-color var(--dur-fast) ease, transform var(--dur-fast) var(--ease-out);
705
+ }
706
+ .live-card:hover{ border-color: rgba(255,122,104,.4); transform: translateY(-1px); }
707
+ .live-card .lc-avatar{
708
+ width:48px; height:48px; border-radius:50%; overflow:hidden; flex-shrink:0;
709
+ border:1px solid var(--border-2); background:var(--bg-1);
710
+ }
711
+ .live-card .lc-avatar svg{ width:100%; height:100%; display:block; }
712
+ .live-card .lc-body{ flex:1; min-width:0; }
713
+ .live-card .lc-handle-row{
714
+ display:flex; align-items:center; gap:6px; flex-wrap:wrap; margin-bottom:4px;
715
+ }
716
+ .live-card .lc-handle{ font-weight:700; font-size:.92rem; word-break:break-all; }
717
+ .live-card .lc-meta{
718
+ display:flex; align-items:center; gap:6px; flex-wrap:wrap;
719
+ font-size:.72rem; color: var(--muted-2);
720
+ }
721
+ .live-card .lc-actions{ display:flex; flex-direction:column; gap:6px; flex-shrink:0; }
722
+ .live-card .lc-actions button{
723
+ border:0; border-radius:10px; padding:8px 14px; font-weight:700; font-size:.76rem;
724
+ min-width: 72px;
725
+ transition: transform var(--dur-fast) var(--ease-out), filter var(--dur-fast) ease, border-color var(--dur-fast) ease, color var(--dur-fast) ease;
726
+ }
727
+ .live-card .lc-actions button:active{ transform: scale(.96); }
728
+ .live-card .btn-call{
729
+ background: linear-gradient(180deg, var(--coral), var(--coral-dim)); color: #2a1109;
730
+ box-shadow: 0 8px 18px -10px rgba(255,122,104,.55);
731
+ }
732
+ .live-card .btn-call:hover{ filter: brightness(1.06); }
733
+ .live-card .btn-call:disabled{ opacity:.4; cursor:not-allowed; filter:none; box-shadow:none; }
734
+ .live-card .btn-chat{
735
+ background: transparent; color: var(--muted); border: 1px solid var(--border-2);
736
+ }
737
+ .live-card .btn-chat:hover{ color: var(--fg); border-color: var(--muted-2); }
738
+ .live-card .btn-chat.has-unread{
739
+ color: var(--coral); border-color: rgba(255,122,104,.45);
740
+ background: rgba(255,122,104,.08);
741
+ }
742
+
743
+ .live-empty{
744
+ border: 1px dashed var(--border-2); border-radius: 16px;
745
+ padding: 18px 16px; text-align:center;
746
+ background: rgba(0,0,0,.14);
747
+ }
748
+ .live-empty strong{ display:block; font-size:.9rem; margin-bottom:4px; }
749
+ .live-empty p{ margin:0; font-size:.8rem; color: var(--muted-2); line-height:1.45; }
750
+
751
+ .live-now-wrap.has-live .section-head h2 .pulse-live{
752
+ width:8px; height:8px; border-radius:50%; background: var(--coral);
753
+ box-shadow: 0 0 0 3px rgba(255,122,104,.2);
754
+ animation: pulse-dot 2s ease-in-out infinite; display:inline-block;
755
+ }
756
+
757
+ /* Demo badge on match cards */
758
+ .mc-demo-badge{
759
+ position:absolute; top:14px; right:14px; z-index:2;
760
+ }
761
+ .match-card{ position:absolute; /* ensure badge anchors */ }
762
+ .mc-handle-row{ display:flex; align-items:center; gap:7px; flex-wrap:wrap; }
763
+ .demo-note{
764
+ font-size:.74rem; color: var(--muted-2); margin: 0 0 10px; line-height:1.4;
765
+ }
766
+ .demo-note b{ color: var(--muted); font-weight:600; }
767
+
768
+ /* Stack polish when demo secondary */
769
+ .stack-wrap.is-secondary{ opacity: .92; }
770
+ .people-col .stack-wrap{ height: 430px; }
771
+
772
+ /* Video / room / chat polish */
773
+ .video-modal .vtile video{
774
+ box-shadow: 0 24px 60px -28px rgba(0,0,0,.8), 0 0 0 1px rgba(255,122,104,.12);
775
+ }
776
+ .video-modal .vlabel{
777
+ background: rgba(0,0,0,.35); padding: 4px 10px; border-radius:999px;
778
+ border: 1px solid var(--border);
779
+ }
780
+ .room-video .rv-tile video{
781
+ box-shadow: 0 14px 36px -20px rgba(0,0,0,.7);
782
+ }
783
+ .room-video .rv-head{
784
+ background: rgba(0,0,0,.28); border: 1px solid var(--border);
785
+ border-radius: 12px; padding: 10px 14px;
786
+ }
787
+ .chat-panel{
788
+ border-color: rgba(255,122,104,.18);
789
+ }
790
+ .chat-panel .cp-head{
791
+ background: rgba(0,0,0,.18);
792
+ }
793
+ .live-panel, .room-panel{
794
+ border-color: rgba(255,122,104,.18);
795
+ }
796
+ .live-panel .lp-title .lp-live-count,
797
+ .room-panel .rp-title .rp-count{
798
+ margin-left:auto; font-size:.68rem; font-weight:700; color: var(--muted-2);
799
+ }
800
+
801
+ @media (max-width: 1020px){
802
+ .you-chip{ max-width: 100%; }
803
+ }
804
+ @media (max-width: 480px){
805
+ .live-card{ flex-wrap: wrap; }
806
+ .live-card .lc-actions{ flex-direction: row; width: 100%; }
807
+ .live-card .lc-actions button{ flex:1; }
808
+ }
809
+
629
810
  </style>
630
811
  </head>
631
812
  <body>
@@ -635,7 +816,16 @@ var webAppHtml = `<!DOCTYPE html>
635
816
  <span class="name">vibe<span>dating</span></span>
636
817
  <span class="tag">matched by how hard you push the model</span>
637
818
  </div>
638
- <div class="local-badge"><span class="dot" aria-hidden="true"></span> raw usage stays local &middot; only league shared</div>
819
+ <div class="topbar-right">
820
+ <div class="you-chip" id="youChip" aria-live="polite">
821
+ <div class="yc-avatar" id="youChipAvatar"></div>
822
+ <div class="yc-meta">
823
+ <div class="yc-handle" id="youChipHandle">@you</div>
824
+ <div class="yc-sub" id="youChipSub"></div>
825
+ </div>
826
+ </div>
827
+ <div class="local-badge"><span class="dot" aria-hidden="true"></span> raw usage stays local &middot; only league shared</div>
828
+ </div>
639
829
  </header>
640
830
 
641
831
  <div class="hero">
@@ -729,12 +919,27 @@ var webAppHtml = `<!DOCTYPE html>
729
919
  </div>
730
920
  </section>
731
921
 
732
- <section class="panel" aria-label="Match stack">
733
- <h2 class="panel-title">Match stack</h2>
734
- <div class="stack-wrap">
735
- <div class="card-stack" id="cardStack"></div>
736
- </div>
737
- </section>
922
+ <div class="people-col panel" aria-label="People">
923
+ <section class="live-now-wrap" id="liveNowWrap" aria-label="Live now">
924
+ <div class="section-head">
925
+ <h2><span class="pulse-live" aria-hidden="true"></span> Live now</h2>
926
+ <span class="count" id="liveNowCount">0</span>
927
+ </div>
928
+ <p class="section-sub">Real discovered peers on the wire &mdash; usage-verified and identity marks from their hello.</p>
929
+ <div class="live-roster" id="liveRoster"></div>
930
+ </section>
931
+
932
+ <section aria-label="Demo profiles">
933
+ <div class="section-head">
934
+ <h2>Demo profiles</h2>
935
+ <span class="mark mark-demo" title="Seeded local pool \u2014 not real people">demo</span>
936
+ </div>
937
+ <p class="demo-note">Seeded local filler from <b>CANDIDATES</b> &mdash; not live peers. Swipe to practice matching by league.</p>
938
+ <div class="stack-wrap" id="demoStackWrap">
939
+ <div class="card-stack" id="cardStack"></div>
940
+ </div>
941
+ </section>
942
+ </div>
738
943
 
739
944
  <section class="panel" aria-label="League ladder">
740
945
  <h2 class="panel-title">League ladder</h2>
@@ -770,10 +975,10 @@ var webAppHtml = `<!DOCTYPE html>
770
975
  </div>
771
976
  </div>
772
977
 
773
- <aside class="live-panel" id="livePanel" aria-label="Live peers">
774
- <div class="lp-title"><span class="lp-dot" aria-hidden="true"></span> Live peers</div>
978
+ <aside class="live-panel" id="livePanel" aria-label="Live peers compact">
979
+ <div class="lp-title"><span class="lp-dot" aria-hidden="true"></span> Live now <span class="lp-live-count" id="lpLiveCount"></span></div>
775
980
  <div id="liveRows"></div>
776
- <div class="lp-legend">&#10003; usage verified &middot; &#128273; identity verified &mdash; call someone, or wait for a call</div>
981
+ <div class="lp-legend">&#10003; usage verified &middot; &#128273; identity verified &mdash; real peers only (not demo)</div>
777
982
  </aside>
778
983
 
779
984
  <div class="incoming-call" id="incomingCall" role="dialog" aria-modal="true" aria-label="Incoming call">
@@ -791,7 +996,7 @@ var webAppHtml = `<!DOCTYPE html>
791
996
  <div class="cp-head">
792
997
  <div>
793
998
  <div class="cp-title" id="chatTitle">@peer</div>
794
- <div class="cp-sub" id="chatSub">live over the P2P link &middot; text only</div>
999
+ <div class="cp-sub" id="chatSub">live peer &middot; P2P text</div>
795
1000
  </div>
796
1001
  <button class="cp-close" id="chatClose" type="button" aria-label="Close chat">&times;</button>
797
1002
  </div>
@@ -811,7 +1016,7 @@ var webAppHtml = `<!DOCTYPE html>
811
1016
  </div>
812
1017
 
813
1018
  <aside class="room-panel" id="roomPanel" aria-label="Room">
814
- <div class="rp-title"><span class="lp-dot" aria-hidden="true"></span> Room</div>
1019
+ <div class="rp-title"><span class="lp-dot" aria-hidden="true"></span> Room <span class="rp-count" id="rpCount"></span></div>
815
1020
  <div class="rp-meta" id="roomMeta">not in a room</div>
816
1021
  <div class="rp-actions">
817
1022
  <button class="rp-btn" id="roomJoinBtn" type="button">Join video</button>
@@ -868,6 +1073,12 @@ var webAppHtml = `<!DOCTYPE html>
868
1073
  function fmt(n){ return Math.round(n).toLocaleString("en-US"); }
869
1074
  function variantFor(handle){ var h=0; for (var i=0;i<handle.length;i++){ h=(h*31+handle.charCodeAt(i))>>>0; } return h % BLOB_PATHS.length; }
870
1075
 
1076
+ // Shared with the live A/V script (separate IIFE) \u2014 roster cards need avatars + league colors.
1077
+ window.__vdAvatarSVG = avatarSVG;
1078
+ window.__vdVariantFor = variantFor;
1079
+ window.__vdLeagueById = leagueById;
1080
+ window.__vdLeagueColorHex = leagueColorHex;
1081
+
871
1082
  var state = { connected:false };
872
1083
  var stackIndex = 0;
873
1084
  var provider = "claude-code";
@@ -932,17 +1143,54 @@ var webAppHtml = `<!DOCTYPE html>
932
1143
  state = s || { connected:false };
933
1144
  if (state.connected){
934
1145
  renderReveal(state);
1146
+ renderYouChip(state);
935
1147
  renderYouMarker(state.league);
936
1148
  stackIndex = 0;
937
1149
  renderStack();
938
1150
  showStep("reveal");
939
1151
  } else {
1152
+ renderYouChip(null);
940
1153
  stackIndex = 0;
941
1154
  renderStack();
942
1155
  showStep("idle");
943
1156
  }
944
1157
  }
945
1158
 
1159
+ function renderYouChip(s){
1160
+ var chip = document.getElementById("youChip");
1161
+ if (!chip) return;
1162
+ if (!s || !s.connected){
1163
+ chip.classList.remove("is-on");
1164
+ return;
1165
+ }
1166
+ var lg = leagueById(s.league);
1167
+ var hex = lg ? lg.hex : "#ffcf6b";
1168
+ document.getElementById("youChipAvatar").innerHTML = avatarSVG(3, hex);
1169
+ document.getElementById("youChipHandle").textContent = s.handle || "@you";
1170
+ var sub = document.getElementById("youChipSub");
1171
+ sub.innerHTML = "";
1172
+ var pill = document.createElement("span");
1173
+ pill.className = "lg-pill";
1174
+ pill.style.background = "color-mix(in srgb, " + hex + " 16%, transparent)";
1175
+ pill.style.border = "1px solid color-mix(in srgb, " + hex + " 40%, transparent)";
1176
+ pill.style.color = hex;
1177
+ pill.textContent = lg ? lg.label : ((s.league || "?") + " League");
1178
+ sub.appendChild(pill);
1179
+ if (s.verified){
1180
+ var vm = document.createElement("span");
1181
+ vm.className = "mark mark-verified";
1182
+ vm.textContent = "\u2713 verified";
1183
+ sub.appendChild(vm);
1184
+ } else {
1185
+ var lm = document.createElement("span");
1186
+ lm.className = "mark mark-demo";
1187
+ lm.textContent = "local";
1188
+ lm.title = "Usage read locally / self-reported";
1189
+ sub.appendChild(lm);
1190
+ }
1191
+ chip.classList.add("is-on");
1192
+ }
1193
+
946
1194
  function renderReveal(s){
947
1195
  var lg = leagueById(s.league);
948
1196
  var hex = lg ? lg.hex : "#ffcf6b";
@@ -992,6 +1240,7 @@ var webAppHtml = `<!DOCTYPE html>
992
1240
  rawToggle.setAttribute("aria-expanded","false");
993
1241
  rawToggle.textContent = "show raw usage (visible only to you)";
994
1242
  document.querySelectorAll(".you-marker").forEach(function(m){ m.remove(); });
1243
+ renderYouChip(null);
995
1244
  stackIndex = 0;
996
1245
  renderStack();
997
1246
  showStep("idle");
@@ -1039,8 +1288,8 @@ var webAppHtml = `<!DOCTYPE html>
1039
1288
  if (!state.connected){
1040
1289
  empty.innerHTML = "<strong>Connect to see matches.</strong><p>Read your usage to get sorted into a league and start matching.</p>";
1041
1290
  } else {
1042
- empty.innerHTML = "<strong>That's everyone in range.</strong><p>You've matched through today's pool. It refreshes with the next billing cycle.</p>" +
1043
- '<button class="btn btn-primary" id="btnRestart" type="button" style="width:auto;padding:11px 20px;">Start over</button>';
1291
+ empty.innerHTML = "<strong>End of demo pool.</strong><p>These were seeded demo profiles \u2014 not live peers. Real people show up under Live now.</p>" +
1292
+ '<button class="btn btn-primary" id="btnRestart" type="button" style="width:auto;padding:11px 20px;">Restart demo</button>';
1044
1293
  }
1045
1294
  cardStackEl.appendChild(empty);
1046
1295
  var restart = empty.querySelector("#btnRestart");
@@ -1056,12 +1305,13 @@ var webAppHtml = `<!DOCTYPE html>
1056
1305
  var label = lg ? lg.label : (cand.league + " League");
1057
1306
  var bio = cand.bio || [];
1058
1307
  el.innerHTML =
1308
+ '<span class="mc-demo-badge mark mark-demo" title="Seeded demo profile \u2014 not a live peer">demo</span>' +
1059
1309
  '<div class="mc-top">' +
1060
1310
  '<div class="mc-avatar">' + avatarSVG(variantFor(cand.handle), hex) + '</div>' +
1061
1311
  '<div>' +
1062
- '<div class="mc-handle">' + cand.handle + '</div>' +
1312
+ '<div class="mc-handle-row"><div class="mc-handle">' + cand.handle + '</div></div>' +
1063
1313
  '<span class="mc-league" style="background:color-mix(in srgb,' + hex + ' 16%, transparent);border:1px solid color-mix(in srgb,' + hex + ' 40%, transparent);color:' + hex + '">' + label + '</span>' +
1064
- '<div class="mc-verified">&#10003; usage verified</div>' +
1314
+ '<div class="mc-verified" style="color:var(--muted-2)">seeded local profile</div>' +
1065
1315
  '</div>' +
1066
1316
  '</div>' +
1067
1317
  '<div class="mc-bio"><p>' + (bio[0] || "") + '</p><p>' + (bio[1] || "") + '</p></div>' +
@@ -1069,7 +1319,7 @@ var webAppHtml = `<!DOCTYPE html>
1069
1319
  '<div class="mc-actions">' +
1070
1320
  '<button class="round-btn pass" type="button" title="Pass">&#10005;</button>' +
1071
1321
  '<button class="round-btn like" type="button" title="Like">&#9829;</button>' +
1072
- '<span class="hint"></span>' +
1322
+ '<span class="hint">demo</span>' +
1073
1323
  '</div>' : '');
1074
1324
  cardStackEl.appendChild(el);
1075
1325
  if (depth === 0){
@@ -1469,7 +1719,7 @@ var webAppHtml = `<!DOCTYPE html>
1469
1719
  delete unread[handle];
1470
1720
  chatTitle.textContent = handle;
1471
1721
  chatSub.textContent = peerKnown(handle)
1472
- ? "live over the P2P link \xB7 text only"
1722
+ ? "live peer \xB7 P2P text \xB7 not a demo profile"
1473
1723
  : "peer offline \xB7 messages will not deliver";
1474
1724
  chatPanel.classList.add("is-open");
1475
1725
  renderChat();
@@ -1531,14 +1781,117 @@ var webAppHtml = `<!DOCTYPE html>
1531
1781
  // Render the live-peers rows: one per connected peer with its verification
1532
1782
  // marks, a Chat button (opens the conversation), and a Call button that
1533
1783
  // rings THAT peer specifically.
1784
+ // Marks from the peer's hello: \u2713 usage verified (real logs), \u{1F511} identity (signed).
1785
+ function peerMarksHtml(p){
1786
+ var html = "";
1787
+ if (p.verified === true) html += '<span class="mark mark-verified" title="Usage verified from real local logs">\u2713 verified</span>';
1788
+ if (p.identityVerified === true) html += '<span class="mark mark-identity" title="Identity signature verified">\u{1F511} identity</span>';
1789
+ return html;
1790
+ }
1791
+
1792
+ function leaguePillHtml(leagueId){
1793
+ var FALLBACK = { "1M":"#d69a6e","5M":"#cfd8e6","10M":"#ffcf6b","100M":"#f0839c","1B+":"#cba8ff" };
1794
+ var LABELS = { "1M":"1M League","5M":"5M League","10M":"10M League","100M":"100M League","1B+":"1B+ League" };
1795
+ var lg = typeof window.__vdLeagueById === "function" ? window.__vdLeagueById(leagueId) : null;
1796
+ var hex = lg ? lg.hex : (FALLBACK[leagueId] || "#ffcf6b");
1797
+ var label = lg ? lg.label : (LABELS[leagueId] || ((leagueId || "?") + " League"));
1798
+ return '<span class="lg-pill" style="background:color-mix(in srgb,' + hex + ' 16%, transparent);border:1px solid color-mix(in srgb,' + hex + ' 40%, transparent);color:' + hex + '">' + label + '</span>';
1799
+ }
1800
+
1801
+ function avatarForPeer(handle, leagueId){
1802
+ var FALLBACK = { "1M":"#d69a6e","5M":"#cfd8e6","10M":"#ffcf6b","100M":"#f0839c","1B+":"#cba8ff" };
1803
+ var hex = (typeof window.__vdLeagueColorHex === "function")
1804
+ ? window.__vdLeagueColorHex(leagueId)
1805
+ : (FALLBACK[leagueId] || "#ffcf6b");
1806
+ if (typeof window.__vdAvatarSVG === "function" && typeof window.__vdVariantFor === "function") {
1807
+ return window.__vdAvatarSVG(window.__vdVariantFor(handle), hex);
1808
+ }
1809
+ return "";
1810
+ }
1811
+
1812
+ // Compact dock rows (floating panel) + full Live now roster cards (main column).
1534
1813
  function renderLiveRows(peers){
1535
1814
  liveRows.innerHTML = "";
1815
+ var roster = document.getElementById("liveRoster");
1816
+ var wrap = document.getElementById("liveNowWrap");
1817
+ var countEl = document.getElementById("liveNowCount");
1818
+ var lpCount = document.getElementById("lpLiveCount");
1819
+ var demoWrap = document.getElementById("demoStackWrap");
1820
+
1821
+ if (countEl) countEl.textContent = String(peers.length);
1822
+ if (lpCount) lpCount.textContent = peers.length ? (peers.length + " online") : "";
1823
+ if (wrap) wrap.classList.toggle("has-live", peers.length > 0);
1824
+ if (demoWrap) demoWrap.classList.toggle("is-secondary", peers.length > 0);
1825
+
1826
+ if (roster) {
1827
+ roster.innerHTML = "";
1828
+ if (peers.length === 0) {
1829
+ var empty = document.createElement("div");
1830
+ empty.className = "live-empty";
1831
+ empty.innerHTML = "<strong>No live peers right now</strong><p>When someone on your league topic connects, they show up here \u2014 clearly separate from the demo pool below.</p>";
1832
+ roster.appendChild(empty);
1833
+ } else {
1834
+ peers.forEach(function(p){
1835
+ var card = document.createElement("article");
1836
+ card.className = "live-card";
1837
+
1838
+ var av = document.createElement("div");
1839
+ av.className = "lc-avatar";
1840
+ av.innerHTML = avatarForPeer(p.handle, p.league);
1841
+
1842
+ var body = document.createElement("div");
1843
+ body.className = "lc-body";
1844
+ var handleRow = document.createElement("div");
1845
+ handleRow.className = "lc-handle-row";
1846
+ var handleEl = document.createElement("div");
1847
+ handleEl.className = "lc-handle";
1848
+ handleEl.textContent = p.handle;
1849
+ handleRow.appendChild(handleEl);
1850
+ var liveMark = document.createElement("span");
1851
+ liveMark.className = "mark mark-live";
1852
+ liveMark.textContent = "live";
1853
+ handleRow.appendChild(liveMark);
1854
+ body.appendChild(handleRow);
1855
+
1856
+ var meta = document.createElement("div");
1857
+ meta.className = "lc-meta";
1858
+ meta.innerHTML = leaguePillHtml(p.league) + peerMarksHtml(p) +
1859
+ '<span style="opacity:.75">' + (p.harness || "") + "</span>";
1860
+ body.appendChild(meta);
1861
+
1862
+ var actions = document.createElement("div");
1863
+ actions.className = "lc-actions";
1864
+ var chatBtn = document.createElement("button");
1865
+ chatBtn.className = "btn-chat" + (unread[p.handle] ? " has-unread" : "");
1866
+ chatBtn.type = "button";
1867
+ chatBtn.textContent = "Chat" + (unread[p.handle] ? " (" + unread[p.handle] + ")" : "");
1868
+ chatBtn.addEventListener("click", function(){ openChat(p.handle); });
1869
+ var callBtn = document.createElement("button");
1870
+ callBtn.className = "btn-call";
1871
+ callBtn.type = "button";
1872
+ callBtn.textContent = "Call";
1873
+ callBtn.disabled = !!(rtc.remoteHandle || pendingOffer);
1874
+ callBtn.addEventListener("click", function(){
1875
+ if (rtc.remoteHandle || pendingOffer) return;
1876
+ callBtn.disabled = true;
1877
+ startCallAsOfferer(p.handle).catch(function(){ hangup(); });
1878
+ });
1879
+ actions.appendChild(chatBtn);
1880
+ actions.appendChild(callBtn);
1881
+
1882
+ card.appendChild(av);
1883
+ card.appendChild(body);
1884
+ card.appendChild(actions);
1885
+ roster.appendChild(card);
1886
+ });
1887
+ }
1888
+ }
1889
+
1890
+ // Compact floating dock \u2014 still handy when scrolled away from the roster.
1536
1891
  peers.forEach(function(p){
1537
1892
  var row = document.createElement("div");
1538
1893
  row.className = "live-row";
1539
1894
  var who = document.createElement("div");
1540
- // Marks from the peer's hello, shown only when present: \u2713 usage
1541
- // verified (real local logs), \u{1F511} identity-verified (signed hello).
1542
1895
  var marks = (p.verified === true ? " \u2713" : "") + (p.identityVerified === true ? " \u{1F511}" : "");
1543
1896
  var h = document.createElement("div"); h.className = "h"; h.textContent = p.handle + marks;
1544
1897
  var s = document.createElement("div"); s.className = "s"; s.textContent = p.league + " \xB7 " + p.harness;
@@ -1553,9 +1906,8 @@ var webAppHtml = `<!DOCTYPE html>
1553
1906
  var btn = document.createElement("button");
1554
1907
  btn.className = "vbtn"; btn.type = "button"; btn.textContent = "Call";
1555
1908
  btn.addEventListener("click", function(){
1556
- if (rtc.remoteHandle || pendingOffer) return; // already in a call / prompt
1909
+ if (rtc.remoteHandle || pendingOffer) return;
1557
1910
  btn.disabled = true;
1558
- // DIRECTED call: the rtc-offer is relayed only to this peer's handle.
1559
1911
  startCallAsOfferer(p.handle).catch(function(){ hangup(); });
1560
1912
  });
1561
1913
  actions.appendChild(chatBtn);
@@ -1576,14 +1928,16 @@ var webAppHtml = `<!DOCTYPE html>
1576
1928
  peers.forEach(function(p){ ensureChatLoop(p.handle); });
1577
1929
  if (chatWith) {
1578
1930
  chatSub.textContent = peerKnown(chatWith)
1579
- ? "live over the P2P link \xB7 text only"
1931
+ ? "live peer \xB7 P2P text"
1580
1932
  : "peer offline \xB7 messages will not deliver";
1581
1933
  }
1582
- if (peers.length === 0) { livePanel.classList.remove("is-open"); return; }
1583
- livePanel.classList.add("is-open");
1584
1934
  renderLiveRows(peers);
1935
+ if (peers.length === 0) { livePanel.classList.remove("is-open"); }
1936
+ else { livePanel.classList.add("is-open"); }
1585
1937
  } catch(e){}
1586
1938
  }
1939
+ // Initial empty state for the main Live now section, then poll.
1940
+ renderLiveRows([]);
1587
1941
  refreshPeers();
1588
1942
  setInterval(refreshPeers, 4000);
1589
1943
 
@@ -1824,6 +2178,8 @@ var webAppHtml = `<!DOCTYPE html>
1824
2178
  roomMeta.textContent = roomName +
1825
2179
  (roomVid.self ? " \xB7 you " + roomVid.self : "") +
1826
2180
  " \xB7 " + n + " other" + (n === 1 ? "" : "s");
2181
+ var rpCount = document.getElementById("rpCount");
2182
+ if (rpCount) rpCount.textContent = n ? (n + " other" + (n === 1 ? "" : "s")) : "empty";
1827
2183
  roomPanel.classList.add("is-open");
1828
2184
  roomJoinBtn.disabled = roomVid.joined || !roomVid.self;
1829
2185
  roomLeaveBtn.disabled = !roomVid.joined;
@@ -2428,7 +2784,7 @@ async function handle(req, res, opts) {
2428
2784
  }
2429
2785
 
2430
2786
  // src/cli.ts
2431
- var VERSION = "0.8.1";
2787
+ var VERSION = "0.8.3";
2432
2788
  function parsePort(raw) {
2433
2789
  const n = Number(raw);
2434
2790
  if (!Number.isInteger(n) || n < 1 || n > 65535) return void 0;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { UsageSnapshot, UsageSource, Harness, HarnessUsageOptions } from '@pooriaarab/vibe-core';
2
2
  export { Harness, UsageSnapshot, UsageSource, createConsentLedger } from '@pooriaarab/vibe-core';
3
- import { P as PeerHello, a as PeerLink } from './room-BStsMaeT.js';
4
- export { D as DiscoveryOptions, b as DiscoverySession, L as LIVE_NOTICE, R as ROOM_TOPIC_PREFIX, c as RoomMember, d as RoomMessage, e as RoomOptions, f as RoomSession, S as StoredPeer, T as TOPIC_PREFIX, l as leagueTopic, g as loadPeers, p as parseHandshake, r as recordPeer, h as recordPeerMessage, i as roomTopic, s as serializeHandshake, j as startDiscovery, k as startRoom } from './room-BStsMaeT.js';
3
+ import { P as PeerHello, a as PeerLink } from './room-DpZhNvuu.js';
4
+ export { D as DiscoveryOptions, b as DiscoverySession, L as LIVE_NOTICE, R as ROOM_TOPIC_PREFIX, c as RoomMember, d as RoomMessage, e as RoomOptions, f as RoomSession, S as StoredPeer, T as TOPIC_PREFIX, l as leagueTopic, g as loadPeers, p as parseHandshake, r as recordPeer, h as recordPeerMessage, i as roomTopic, s as serializeHandshake, j as startDiscovery, k as startRoom } from './room-DpZhNvuu.js';
5
5
  import { KeyObject } from 'node:crypto';
6
6
 
7
7
  /**
package/dist/index.js CHANGED
@@ -36,7 +36,7 @@ import {
36
36
  startDiscovery,
37
37
  startRoom,
38
38
  verifyHelloClaims
39
- } from "./chunk-S7MO5OSO.js";
39
+ } from "./chunk-3FJDVN72.js";
40
40
  export {
41
41
  BELOW_LEAGUE,
42
42
  CANDIDATES,
package/dist/mcp.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { a as PeerLink, P as PeerHello, f as RoomSession, b as DiscoverySession, d as RoomMessage } from './room-BStsMaeT.js';
2
+ import { a as PeerLink, P as PeerHello, f as RoomSession, b as DiscoverySession, d as RoomMessage } from './room-DpZhNvuu.js';
3
3
  import '@pooriaarab/vibe-core';
4
4
 
5
5
  /**
package/dist/mcp.js CHANGED
@@ -3,8 +3,8 @@ import {
3
3
  createMcpServer,
4
4
  createSessionState,
5
5
  runMcp
6
- } from "./chunk-JBADPOR5.js";
7
- import "./chunk-S7MO5OSO.js";
6
+ } from "./chunk-XUWNLMOY.js";
7
+ import "./chunk-3FJDVN72.js";
8
8
  export {
9
9
  MCP_TOOL_NAMES,
10
10
  createMcpServer,
@@ -149,10 +149,10 @@ interface StoredPeer extends PeerHello {
149
149
  readonly lastMessageAt?: string;
150
150
  }
151
151
  /** Load persisted live peers, or `[]` if none/corrupt. Local-only data. */
152
- declare function loadPeers(dir?: string): StoredPeer[];
152
+ declare function loadPeers(dir?: string, maxAgeMs?: number): StoredPeer[];
153
153
  /**
154
- * Record a successfully handshaken peer, keyed by handle (a peer may reconnect
155
- * from a different key). Returns whether this handle is NEW (first time seen).
154
+ * Record a successfully handshaken peer, keyed by pubkey (if verified) or handle (legacy).
155
+ * Returns whether this peer is NEW (first time seen).
156
156
  */
157
157
  declare function recordPeer(hello: PeerHello, dir?: string, now?: Date): {
158
158
  peer: StoredPeer;
@@ -163,7 +163,7 @@ declare function recordPeer(hello: PeerHello, dir?: string, now?: Date): {
163
163
  * Local metadata only; never on the wire. Returns false when the handle isn't
164
164
  * a known peer. Never throws — best-effort bookkeeping.
165
165
  */
166
- declare function recordPeerMessage(handle: string, dir?: string, now?: Date): boolean;
166
+ declare function recordPeerMessage(peer: PeerHello, dir?: string, now?: Date): boolean;
167
167
  /** Injection point for the match notification (defaults to vibe-core notify). */
168
168
  type NotifySink = (event: VibeEvent) => void;
169
169
  interface DiscoveryOptions {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibedate",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
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",