vibedate 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -5
- package/dist/{chunk-64ME4THO.js → chunk-5ZJIPMU6.js} +2 -65
- package/dist/chunk-6NWF2VD7.js +744 -0
- package/dist/cli.d.ts +5 -1
- package/dist/cli.js +723 -44
- package/dist/index.d.ts +184 -2
- package/dist/index.js +17 -1
- package/dist/mcp.js +2 -2
- package/package.json +5 -3
- package/dist/chunk-AU7FN2LY.js +0 -133
package/dist/cli.js
CHANGED
|
@@ -1,22 +1,103 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
connectProfile,
|
|
4
|
-
loadProfile,
|
|
5
3
|
runMcp
|
|
6
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-5ZJIPMU6.js";
|
|
7
5
|
import {
|
|
8
6
|
CANDIDATES,
|
|
7
|
+
LIVE_NOTICE,
|
|
8
|
+
TOPIC_PREFIX,
|
|
9
|
+
canShareLive,
|
|
10
|
+
connectProfile,
|
|
11
|
+
grantLiveConsent,
|
|
9
12
|
league,
|
|
13
|
+
leagueIndex,
|
|
14
|
+
loadPeers,
|
|
15
|
+
loadProfile,
|
|
10
16
|
matches,
|
|
11
|
-
|
|
12
|
-
|
|
17
|
+
parseFrame,
|
|
18
|
+
readUsage,
|
|
19
|
+
startDiscovery
|
|
20
|
+
} from "./chunk-6NWF2VD7.js";
|
|
13
21
|
|
|
14
22
|
// src/cli.ts
|
|
15
|
-
import
|
|
16
|
-
import
|
|
23
|
+
import readline from "readline";
|
|
24
|
+
import "url";
|
|
25
|
+
import process2 from "process";
|
|
26
|
+
|
|
27
|
+
// src/pairing.ts
|
|
28
|
+
function createPairing() {
|
|
29
|
+
const queue = [];
|
|
30
|
+
const matchCbs = /* @__PURE__ */ new Set();
|
|
31
|
+
let current;
|
|
32
|
+
const emit = (link) => {
|
|
33
|
+
for (const cb of matchCbs) cb(link);
|
|
34
|
+
};
|
|
35
|
+
const watch = (link) => {
|
|
36
|
+
link.onClose(() => {
|
|
37
|
+
if (current === link) {
|
|
38
|
+
current = void 0;
|
|
39
|
+
const nextUp = queue.shift();
|
|
40
|
+
if (nextUp !== void 0) {
|
|
41
|
+
current = nextUp;
|
|
42
|
+
emit(current);
|
|
43
|
+
} else {
|
|
44
|
+
emit(void 0);
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
const idx = queue.indexOf(link);
|
|
48
|
+
if (idx >= 0) queue.splice(idx, 1);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
get available() {
|
|
54
|
+
return queue.length;
|
|
55
|
+
},
|
|
56
|
+
current() {
|
|
57
|
+
return current;
|
|
58
|
+
},
|
|
59
|
+
add(link) {
|
|
60
|
+
watch(link);
|
|
61
|
+
if (current === void 0) {
|
|
62
|
+
current = link;
|
|
63
|
+
emit(link);
|
|
64
|
+
} else {
|
|
65
|
+
queue.push(link);
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
next() {
|
|
69
|
+
if (current !== void 0) {
|
|
70
|
+
current.close();
|
|
71
|
+
current = void 0;
|
|
72
|
+
}
|
|
73
|
+
const nextUp = queue.shift();
|
|
74
|
+
if (nextUp !== void 0) {
|
|
75
|
+
current = nextUp;
|
|
76
|
+
}
|
|
77
|
+
emit(current);
|
|
78
|
+
return current;
|
|
79
|
+
},
|
|
80
|
+
open(handle2) {
|
|
81
|
+
const idx = queue.findIndex((l) => l.hello.handle === handle2);
|
|
82
|
+
if (idx < 0) return void 0;
|
|
83
|
+
const link = queue.splice(idx, 1)[0];
|
|
84
|
+
if (current !== void 0) {
|
|
85
|
+
current.close();
|
|
86
|
+
current = void 0;
|
|
87
|
+
}
|
|
88
|
+
current = link;
|
|
89
|
+
emit(current);
|
|
90
|
+
return current;
|
|
91
|
+
},
|
|
92
|
+
onMatch(cb) {
|
|
93
|
+
matchCbs.add(cb);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
17
97
|
|
|
18
98
|
// src/server.ts
|
|
19
99
|
import http from "http";
|
|
100
|
+
import { makeEvent, notify as vibeCoreNotify } from "@pooriaarab/vibe-core";
|
|
20
101
|
|
|
21
102
|
// src/web-app-html.ts
|
|
22
103
|
var webAppHtml = `<!DOCTYPE html>
|
|
@@ -408,6 +489,49 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
408
489
|
.stage{ padding: 22px 18px 80px; }
|
|
409
490
|
header.topbar{ padding: 13px 16px; flex-wrap: wrap; }
|
|
410
491
|
}
|
|
492
|
+
|
|
493
|
+
/* ---- live A/V (browser WebRTC over the local signaling relay) ---- */
|
|
494
|
+
.live-panel{
|
|
495
|
+
position: fixed; left: 18px; bottom: 18px; z-index: 45;
|
|
496
|
+
background: linear-gradient(180deg, var(--bg-card), var(--bg-card-2));
|
|
497
|
+
border: 1px solid var(--border-2); border-radius: 16px; box-shadow: var(--shadow-2);
|
|
498
|
+
padding: 12px 14px; min-width: 220px; max-width: 260px; display: none;
|
|
499
|
+
}
|
|
500
|
+
.live-panel.is-open{ display: block; animation: rise var(--dur-med) var(--ease-out); }
|
|
501
|
+
.live-panel .lp-title{ font-size: .78rem; font-weight: 800; margin-bottom: 8px; display: flex; align-items: center; gap: 6px; }
|
|
502
|
+
.live-panel .lp-dot{ width: 7px; height: 7px; border-radius: 50%; background: var(--coral); box-shadow: 0 0 0 3px rgba(255,122,104,.18); flex-shrink: 0; }
|
|
503
|
+
.live-row{ display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 7px 0; border-top: 1px solid var(--border); }
|
|
504
|
+
.live-row:first-of-type{ border-top: 0; }
|
|
505
|
+
.live-row .h{ font-size: .84rem; font-weight: 600; }
|
|
506
|
+
.live-row .s{ font-size: .7rem; color: var(--muted-2); }
|
|
507
|
+
.live-row .vbtn{
|
|
508
|
+
border: 0; border-radius: 9px; padding: 7px 12px; font-weight: 700; font-size: .76rem;
|
|
509
|
+
background: linear-gradient(180deg, var(--coral), var(--coral-dim)); color: #2a1109;
|
|
510
|
+
transition: transform var(--dur-fast) var(--ease-out), filter var(--dur-fast) ease;
|
|
511
|
+
flex-shrink: 0;
|
|
512
|
+
}
|
|
513
|
+
.live-row .vbtn:hover{ filter: brightness(1.06); }
|
|
514
|
+
.live-row .vbtn:active{ transform: scale(.95); }
|
|
515
|
+
.live-row .vbtn:disabled{ opacity: .4; cursor: not-allowed; }
|
|
516
|
+
.video-modal{
|
|
517
|
+
position: fixed; inset: 0; z-index: 70;
|
|
518
|
+
display: flex; align-items: center; justify-content: center; gap: 18px; flex-wrap: wrap;
|
|
519
|
+
background: rgba(12,7,15,.78); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
|
|
520
|
+
opacity: 0; pointer-events: none; transition: opacity var(--dur-med) ease;
|
|
521
|
+
}
|
|
522
|
+
.video-modal.is-open{ opacity: 1; pointer-events: auto; }
|
|
523
|
+
.video-modal .vtile{ display: flex; flex-direction: column; align-items: center; gap: 8px; }
|
|
524
|
+
.video-modal video{
|
|
525
|
+
width: min(42vw, 560px); aspect-ratio: 4 / 3; background: #000; border-radius: 16px;
|
|
526
|
+
border: 1px solid var(--border-2); box-shadow: var(--shadow-3); object-fit: cover;
|
|
527
|
+
}
|
|
528
|
+
.video-modal .vlabel{ font-size: .8rem; color: var(--muted); font-weight: 600; }
|
|
529
|
+
.video-modal .vhangup{
|
|
530
|
+
position: absolute; bottom: 28px; left: 50%; transform: translateX(-50%);
|
|
531
|
+
border: 0; border-radius: 999px; padding: 12px 22px; font-weight: 800; font-size: .9rem;
|
|
532
|
+
background: var(--danger); color: #2a0a0c;
|
|
533
|
+
}
|
|
534
|
+
.video-modal .vhangup:hover{ filter: brightness(1.06); }
|
|
411
535
|
</style>
|
|
412
536
|
</head>
|
|
413
537
|
<body>
|
|
@@ -552,6 +676,17 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
552
676
|
</div>
|
|
553
677
|
</div>
|
|
554
678
|
|
|
679
|
+
<aside class="live-panel" id="livePanel" aria-label="Live same-league peers">
|
|
680
|
+
<div class="lp-title"><span class="lp-dot" aria-hidden="true"></span> Live same-league peers</div>
|
|
681
|
+
<div id="liveRows"></div>
|
|
682
|
+
</aside>
|
|
683
|
+
|
|
684
|
+
<div class="video-modal" id="videoModal" role="dialog" aria-modal="true" aria-label="Video call">
|
|
685
|
+
<div class="vtile"><video id="remoteVideo" autoplay playsinline></video><div class="vlabel" id="remoteLabel">remote</div></div>
|
|
686
|
+
<div class="vtile"><video id="localVideo" autoplay playsinline muted></video><div class="vlabel">you</div></div>
|
|
687
|
+
<button class="vhangup" id="hangupBtn" type="button">Hang up</button>
|
|
688
|
+
</div>
|
|
689
|
+
|
|
555
690
|
<script>
|
|
556
691
|
(function(){
|
|
557
692
|
"use strict";
|
|
@@ -833,10 +968,252 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
833
968
|
|
|
834
969
|
})();
|
|
835
970
|
</script>
|
|
971
|
+
|
|
972
|
+
<script>
|
|
973
|
+
(function(){
|
|
974
|
+
"use strict";
|
|
975
|
+
/* ---- Live A/V: browser WebRTC over the local server's signaling relay ----
|
|
976
|
+
* The browser owns the RTCPeerConnection (mic/camera capture, DTLS, SRTP);
|
|
977
|
+
* the local server only ferries rtc-offer / rtc-answer / rtc-ice frames
|
|
978
|
+
* between this browser and the remote peer's PeerLink. No media bytes touch
|
|
979
|
+
* the server, and no native WebRTC dep ships with the CLI. */
|
|
980
|
+
|
|
981
|
+
var livePanel = document.getElementById("livePanel");
|
|
982
|
+
var liveRows = document.getElementById("liveRows");
|
|
983
|
+
var videoModal = document.getElementById("videoModal");
|
|
984
|
+
var localVideo = document.getElementById("localVideo");
|
|
985
|
+
var remoteVideo = document.getElementById("remoteVideo");
|
|
986
|
+
var remoteLabel = document.getElementById("remoteLabel");
|
|
987
|
+
var hangupBtn = document.getElementById("hangupBtn");
|
|
988
|
+
|
|
989
|
+
// One in-flight call's state. remoteHandle === null means idle.
|
|
990
|
+
var rtc = { pc: null, localStream: null, remoteHandle: null, role: null };
|
|
991
|
+
var knownPeers = [];
|
|
992
|
+
var idleIdx = 0;
|
|
993
|
+
|
|
994
|
+
function rtcConfig(){
|
|
995
|
+
// A public STUN server crosses most NATs. No TURN in v0 \u2014 symmetric NATs
|
|
996
|
+
// won't connect, but signaling still completes and the call fails open.
|
|
997
|
+
return { iceServers: [{ urls: "stun:stun.l.google.com:19302" }] };
|
|
998
|
+
}
|
|
999
|
+
function sleep(ms){ return new Promise(function(r){ setTimeout(r, ms); }); }
|
|
1000
|
+
|
|
1001
|
+
// GET /live/signal?handle=X with a client-side abort so we never pile up
|
|
1002
|
+
// concurrent long-polls; on abort/error the caller just retries.
|
|
1003
|
+
function fetchSignal(handle, timeoutMs){
|
|
1004
|
+
var ctrl = new AbortController();
|
|
1005
|
+
var t = setTimeout(function(){ ctrl.abort(); }, timeoutMs);
|
|
1006
|
+
return fetch("/live/signal?handle=" + encodeURIComponent(handle), { signal: ctrl.signal })
|
|
1007
|
+
.then(function(r){ clearTimeout(t); return r; })
|
|
1008
|
+
.catch(function(e){ clearTimeout(t); throw e; });
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
async function postSignal(handle, frame){
|
|
1012
|
+
try {
|
|
1013
|
+
await fetch("/live/signal", {
|
|
1014
|
+
method: "POST",
|
|
1015
|
+
headers: { "content-type": "application/json" },
|
|
1016
|
+
body: JSON.stringify({ handle: handle, frame: frame })
|
|
1017
|
+
});
|
|
1018
|
+
} catch(e){ /* best effort \u2014 a dropped signal just slows the handshake */ }
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function showVideoModal(remoteHandle){
|
|
1022
|
+
remoteLabel.textContent = remoteHandle;
|
|
1023
|
+
videoModal.classList.add("is-open");
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function attachTracks(pc, stream){
|
|
1027
|
+
stream.getTracks().forEach(function(t){ pc.addTrack(t, stream); });
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// Offerer path: the user clicked our Video button.
|
|
1031
|
+
async function startCallAsOfferer(handle){
|
|
1032
|
+
rtc.role = "offerer";
|
|
1033
|
+
rtc.remoteHandle = handle;
|
|
1034
|
+
rtc.localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
|
|
1035
|
+
localVideo.srcObject = rtc.localStream;
|
|
1036
|
+
var pc = new RTCPeerConnection(rtcConfig());
|
|
1037
|
+
rtc.pc = pc;
|
|
1038
|
+
pc.onicecandidate = onIce;
|
|
1039
|
+
pc.ontrack = function(e){ remoteVideo.srcObject = e.streams[0]; };
|
|
1040
|
+
attachTracks(pc, rtc.localStream);
|
|
1041
|
+
var offer = await pc.createOffer();
|
|
1042
|
+
await pc.setLocalDescription(offer);
|
|
1043
|
+
await postSignal(handle, { t: "rtc-offer", sdp: offer.sdp });
|
|
1044
|
+
showVideoModal(handle);
|
|
1045
|
+
pollSignal(handle);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// Answerer path: we received the peer's offer via the relay.
|
|
1049
|
+
async function answerIncomingOffer(handle, sdp){
|
|
1050
|
+
rtc.role = "answerer";
|
|
1051
|
+
rtc.remoteHandle = handle;
|
|
1052
|
+
rtc.localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
|
|
1053
|
+
localVideo.srcObject = rtc.localStream;
|
|
1054
|
+
var pc = new RTCPeerConnection(rtcConfig());
|
|
1055
|
+
rtc.pc = pc;
|
|
1056
|
+
pc.onicecandidate = onIce;
|
|
1057
|
+
pc.ontrack = function(e){ remoteVideo.srcObject = e.streams[0]; };
|
|
1058
|
+
attachTracks(pc, rtc.localStream);
|
|
1059
|
+
await pc.setRemoteDescription({ type: "offer", sdp: sdp });
|
|
1060
|
+
var answer = await pc.createAnswer();
|
|
1061
|
+
await pc.setLocalDescription(answer);
|
|
1062
|
+
await postSignal(handle, { t: "rtc-answer", sdp: answer.sdp });
|
|
1063
|
+
showVideoModal(handle);
|
|
1064
|
+
pollSignal(handle);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
function onIce(e){
|
|
1068
|
+
if (!rtc.remoteHandle) return;
|
|
1069
|
+
if (e.candidate) {
|
|
1070
|
+
// Trickle ICE: ship each candidate the moment it is gathered.
|
|
1071
|
+
postSignal(rtc.remoteHandle, { t: "rtc-ice", candidate: e.candidate.candidate });
|
|
1072
|
+
} else {
|
|
1073
|
+
// Gathering complete \u2014 send the end-of-candidates marker.
|
|
1074
|
+
postSignal(rtc.remoteHandle, { t: "rtc-ice", candidate: "" });
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// Active long-poll loop for the peer we're in a call with. Drains answer +
|
|
1079
|
+
// ICE candidates until the call ends.
|
|
1080
|
+
async function pollSignal(handle){
|
|
1081
|
+
while (rtc.remoteHandle === handle && rtc.pc && rtc.pc.connectionState !== "closed") {
|
|
1082
|
+
var res;
|
|
1083
|
+
try { res = await fetchSignal(handle, 5000); }
|
|
1084
|
+
catch(e){ await sleep(500); continue; }
|
|
1085
|
+
if (!res || res.status !== 200) { await sleep(500); continue; }
|
|
1086
|
+
var data = await res.json();
|
|
1087
|
+
var f = data && data.frame;
|
|
1088
|
+
if (!f) continue; // timed out empty
|
|
1089
|
+
await handleIncoming(handle, f);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
async function handleIncoming(handle, f){
|
|
1094
|
+
// An offer arriving with no active PC makes us the answerer.
|
|
1095
|
+
if (!rtc.pc) {
|
|
1096
|
+
if (f.t === "rtc-offer") { await answerIncomingOffer(handle, f.sdp); }
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
if (f.t === "rtc-answer") {
|
|
1100
|
+
try { await rtc.pc.setRemoteDescription({ type: "answer", sdp: f.sdp }); } catch(e){}
|
|
1101
|
+
} else if (f.t === "rtc-ice") {
|
|
1102
|
+
try { await rtc.pc.addIceCandidate({ candidate: f.candidate }); } catch(e){}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// Idle listener: when not in a call, watch known peers round-robin so an
|
|
1107
|
+
// incoming offer (the peer clicked THEIR Video button) is noticed and answered.
|
|
1108
|
+
async function idleLoop(){
|
|
1109
|
+
while (true) {
|
|
1110
|
+
if (rtc.remoteHandle || knownPeers.length === 0) { await sleep(1000); continue; }
|
|
1111
|
+
var peer = knownPeers[idleIdx % knownPeers.length];
|
|
1112
|
+
idleIdx++;
|
|
1113
|
+
try {
|
|
1114
|
+
var res = await fetchSignal(peer.handle, 3000);
|
|
1115
|
+
if (res && res.status === 200) {
|
|
1116
|
+
var data = await res.json();
|
|
1117
|
+
var f = data && data.frame;
|
|
1118
|
+
if (f && f.t === "rtc-offer" && !rtc.pc && !rtc.remoteHandle) {
|
|
1119
|
+
await answerIncomingOffer(peer.handle, f.sdp);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
} catch(e){ /* abort/timeout \u2014 loop to next peer */ }
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
idleLoop();
|
|
1126
|
+
|
|
1127
|
+
function hangup(){
|
|
1128
|
+
try { if (rtc.pc) rtc.pc.close(); } catch(e){}
|
|
1129
|
+
rtc.pc = null;
|
|
1130
|
+
rtc.remoteHandle = null;
|
|
1131
|
+
rtc.role = null;
|
|
1132
|
+
if (rtc.localStream) {
|
|
1133
|
+
rtc.localStream.getTracks().forEach(function(t){ t.stop(); });
|
|
1134
|
+
rtc.localStream = null;
|
|
1135
|
+
}
|
|
1136
|
+
localVideo.srcObject = null;
|
|
1137
|
+
remoteVideo.srcObject = null;
|
|
1138
|
+
videoModal.classList.remove("is-open");
|
|
1139
|
+
}
|
|
1140
|
+
hangupBtn.addEventListener("click", hangup);
|
|
1141
|
+
|
|
1142
|
+
// Render the live-peers panel with a Video button each.
|
|
1143
|
+
async function refreshPeers(){
|
|
1144
|
+
try {
|
|
1145
|
+
var res = await fetch("/api/live/peers");
|
|
1146
|
+
if (res.status !== 200) return;
|
|
1147
|
+
var data = await res.json();
|
|
1148
|
+
var peers = (data && data.peers) || [];
|
|
1149
|
+
knownPeers = peers;
|
|
1150
|
+
if (peers.length === 0) { livePanel.classList.remove("is-open"); return; }
|
|
1151
|
+
livePanel.classList.add("is-open");
|
|
1152
|
+
liveRows.innerHTML = "";
|
|
1153
|
+
peers.forEach(function(p){
|
|
1154
|
+
var row = document.createElement("div");
|
|
1155
|
+
row.className = "live-row";
|
|
1156
|
+
var who = document.createElement("div");
|
|
1157
|
+
var h = document.createElement("div"); h.className = "h"; h.textContent = p.handle;
|
|
1158
|
+
var s = document.createElement("div"); s.className = "s"; s.textContent = p.league + " \xB7 " + p.harness;
|
|
1159
|
+
who.appendChild(h); who.appendChild(s);
|
|
1160
|
+
var btn = document.createElement("button");
|
|
1161
|
+
btn.className = "vbtn"; btn.type = "button"; btn.textContent = "Video";
|
|
1162
|
+
btn.addEventListener("click", function(){
|
|
1163
|
+
if (rtc.remoteHandle) return; // already in a call
|
|
1164
|
+
btn.disabled = true;
|
|
1165
|
+
startCallAsOfferer(p.handle).catch(function(){ btn.disabled = false; });
|
|
1166
|
+
});
|
|
1167
|
+
row.appendChild(who); row.appendChild(btn);
|
|
1168
|
+
liveRows.appendChild(row);
|
|
1169
|
+
});
|
|
1170
|
+
} catch(e){}
|
|
1171
|
+
}
|
|
1172
|
+
refreshPeers();
|
|
1173
|
+
setInterval(refreshPeers, 4000);
|
|
1174
|
+
})();
|
|
1175
|
+
</script>
|
|
836
1176
|
</body>
|
|
837
1177
|
</html>`;
|
|
838
1178
|
|
|
839
1179
|
// src/server.ts
|
|
1180
|
+
function createLiveBridge() {
|
|
1181
|
+
const boxes = /* @__PURE__ */ new Map();
|
|
1182
|
+
const bridge = {
|
|
1183
|
+
get peers() {
|
|
1184
|
+
return [...boxes.values()].map((m) => m.link.hello);
|
|
1185
|
+
},
|
|
1186
|
+
addLink(link) {
|
|
1187
|
+
const handle2 = link.hello.handle;
|
|
1188
|
+
boxes.set(handle2, { link, incoming: [] });
|
|
1189
|
+
link.onSignal((f) => {
|
|
1190
|
+
const mb = boxes.get(handle2);
|
|
1191
|
+
if (mb) mb.incoming.push(f);
|
|
1192
|
+
});
|
|
1193
|
+
link.onClose(() => {
|
|
1194
|
+
const cur = boxes.get(handle2);
|
|
1195
|
+
if (cur && cur.link === link) boxes.delete(handle2);
|
|
1196
|
+
});
|
|
1197
|
+
},
|
|
1198
|
+
sendSignal(handle2, frame) {
|
|
1199
|
+
boxes.get(handle2)?.link.sendSignal(frame);
|
|
1200
|
+
},
|
|
1201
|
+
async pollSignal(handle2, timeoutMs) {
|
|
1202
|
+
const mb = boxes.get(handle2);
|
|
1203
|
+
if (!mb) return null;
|
|
1204
|
+
if (mb.incoming.length > 0) return mb.incoming.shift() ?? null;
|
|
1205
|
+
const deadline = Date.now() + timeoutMs;
|
|
1206
|
+
while (Date.now() < deadline) {
|
|
1207
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1208
|
+
const cur = boxes.get(handle2);
|
|
1209
|
+
if (!cur) return null;
|
|
1210
|
+
if (cur.incoming.length > 0) return cur.incoming.shift() ?? null;
|
|
1211
|
+
}
|
|
1212
|
+
return null;
|
|
1213
|
+
}
|
|
1214
|
+
};
|
|
1215
|
+
return bridge;
|
|
1216
|
+
}
|
|
840
1217
|
function currentState(dir) {
|
|
841
1218
|
const p = loadProfile(dir);
|
|
842
1219
|
if (!p) return { connected: false, candidates: [] };
|
|
@@ -914,22 +1291,126 @@ async function handle(req, res, opts) {
|
|
|
914
1291
|
sendJson(res, 200, profileToState(profile));
|
|
915
1292
|
return;
|
|
916
1293
|
}
|
|
1294
|
+
if (req.method === "POST" && pathname === "/api/match") {
|
|
1295
|
+
const body = await readBody(req);
|
|
1296
|
+
let parsed = {};
|
|
1297
|
+
if (body.trim() !== "") {
|
|
1298
|
+
try {
|
|
1299
|
+
parsed = JSON.parse(body);
|
|
1300
|
+
} catch {
|
|
1301
|
+
sendJson(res, 400, { error: "invalid JSON body" });
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
const candidateHandle = typeof parsed["handle"] === "string" ? parsed["handle"] : "";
|
|
1306
|
+
if (candidateHandle === "") {
|
|
1307
|
+
sendJson(res, 400, { error: "missing candidate handle" });
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
const profile = loadProfile(opts.dir);
|
|
1311
|
+
if (!profile) {
|
|
1312
|
+
sendJson(res, 409, { error: "not connected" });
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
const candidate = CANDIDATES.find((c) => c.handle === candidateHandle);
|
|
1316
|
+
if (!candidate) {
|
|
1317
|
+
sendJson(res, 404, { error: "unknown candidate" });
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
const matched = candidate.league === profile.league;
|
|
1321
|
+
if (matched) {
|
|
1322
|
+
const sink = opts.notify ?? vibeCoreNotify;
|
|
1323
|
+
try {
|
|
1324
|
+
sink(
|
|
1325
|
+
makeEvent("match", profile.harness, process.cwd(), {
|
|
1326
|
+
summary: `matched with ${candidate.handle} - SAME LEAGUE`,
|
|
1327
|
+
handle: candidate.handle,
|
|
1328
|
+
league: profile.league
|
|
1329
|
+
})
|
|
1330
|
+
);
|
|
1331
|
+
} catch {
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
sendJson(res, 200, { matched, handle: candidate.handle, league: candidate.league });
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
if (req.method === "GET" && pathname === "/api/live/peers") {
|
|
1338
|
+
const live = opts.live;
|
|
1339
|
+
sendJson(res, 200, { peers: live ? live.peers : [] });
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
if (req.method === "GET" && pathname === "/live/signal") {
|
|
1343
|
+
const live = opts.live;
|
|
1344
|
+
if (!live) {
|
|
1345
|
+
sendJson(res, 200, { frame: null, reason: "live-not-attached" });
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
const handle2 = url.searchParams.get("handle") ?? "";
|
|
1349
|
+
if (handle2 === "") {
|
|
1350
|
+
sendJson(res, 400, { error: "missing handle" });
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
const frame = await live.pollSignal(handle2, 25e3);
|
|
1354
|
+
if (req.destroyed || res.writableEnded) return;
|
|
1355
|
+
sendJson(res, 200, { frame });
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
if (req.method === "POST" && pathname === "/live/signal") {
|
|
1359
|
+
const live = opts.live;
|
|
1360
|
+
if (!live) {
|
|
1361
|
+
sendJson(res, 400, { error: "live-not-attached" });
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
const body = await readBody(req);
|
|
1365
|
+
let parsed = {};
|
|
1366
|
+
try {
|
|
1367
|
+
parsed = JSON.parse(body);
|
|
1368
|
+
} catch {
|
|
1369
|
+
sendJson(res, 400, { error: "invalid JSON body" });
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
const handle2 = typeof parsed["handle"] === "string" ? parsed["handle"] : "";
|
|
1373
|
+
if (handle2 === "") {
|
|
1374
|
+
sendJson(res, 400, { error: "missing handle" });
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
const reParsed = parseFrame(JSON.stringify(parsed["frame"]));
|
|
1378
|
+
if (reParsed === null || reParsed.t !== "rtc-offer" && reParsed.t !== "rtc-answer" && reParsed.t !== "rtc-ice") {
|
|
1379
|
+
sendJson(res, 400, { error: "invalid rtc frame" });
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
live.sendSignal(handle2, reParsed);
|
|
1383
|
+
sendJson(res, 200, { ok: true });
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
917
1386
|
sendJson(res, 404, { error: "not found" });
|
|
918
1387
|
}
|
|
919
1388
|
|
|
920
1389
|
// src/cli.ts
|
|
921
|
-
var VERSION = "0.
|
|
1390
|
+
var VERSION = "0.2.0";
|
|
922
1391
|
function parsePort(raw) {
|
|
923
1392
|
const n = Number(raw);
|
|
924
1393
|
if (!Number.isInteger(n) || n < 1 || n > 65535) return void 0;
|
|
925
1394
|
return n;
|
|
926
1395
|
}
|
|
927
1396
|
function parseArgs(argv) {
|
|
928
|
-
let out = { command: null, port: void 0 };
|
|
1397
|
+
let out = { command: null, port: void 0, live: false, dating: false };
|
|
929
1398
|
for (let i = 0; i < argv.length; i++) {
|
|
930
1399
|
const a = argv[i];
|
|
931
|
-
if (a === "--version" || a === "-v")
|
|
932
|
-
|
|
1400
|
+
if (a === "--version" || a === "-v") {
|
|
1401
|
+
return { command: "version", port: void 0, live: false, dating: false };
|
|
1402
|
+
}
|
|
1403
|
+
if (a === "--help" || a === "-h") {
|
|
1404
|
+
return { command: "help", port: void 0, live: false, dating: false };
|
|
1405
|
+
}
|
|
1406
|
+
if (a === "--live") {
|
|
1407
|
+
out = { ...out, live: true };
|
|
1408
|
+
continue;
|
|
1409
|
+
}
|
|
1410
|
+
if (a === "--dating") {
|
|
1411
|
+
out = { ...out, dating: true };
|
|
1412
|
+
continue;
|
|
1413
|
+
}
|
|
933
1414
|
if (a === "--port") {
|
|
934
1415
|
const next = argv[i + 1];
|
|
935
1416
|
if (next !== void 0) {
|
|
@@ -945,7 +1426,7 @@ function parseArgs(argv) {
|
|
|
945
1426
|
continue;
|
|
946
1427
|
}
|
|
947
1428
|
if (a.startsWith("-")) continue;
|
|
948
|
-
const known = a === "connect" || a === "matches" || a === "open" || a === "mcp" || a === "help" ? a : null;
|
|
1429
|
+
const known = a === "connect" || a === "matches" || a === "discover" || a === "open" || a === "live" || a === "mcp" || a === "help" ? a : null;
|
|
949
1430
|
if (known !== null && out.command === null) {
|
|
950
1431
|
out = { ...out, command: known };
|
|
951
1432
|
}
|
|
@@ -956,70 +1437,264 @@ function leagueLabel(name) {
|
|
|
956
1437
|
return name === "below-1M" ? "below 1M (not yet in a league)" : `${name} League`;
|
|
957
1438
|
}
|
|
958
1439
|
async function cmdConnect() {
|
|
959
|
-
const harness =
|
|
960
|
-
const handle2 =
|
|
1440
|
+
const harness = process2.env["VIBEDATING_HARNESS"] ?? "claude-code";
|
|
1441
|
+
const handle2 = process2.env["VIBEDATING_HANDLE"] ?? "@you";
|
|
961
1442
|
const snapshot = await readUsage(harness);
|
|
962
1443
|
const profile = connectProfile(snapshot, handle2);
|
|
963
1444
|
const lg = league(snapshot.totalTokens);
|
|
964
|
-
|
|
965
|
-
|
|
1445
|
+
process2.stdout.write("\n");
|
|
1446
|
+
process2.stdout.write(` ${leagueLabel(lg.name)}
|
|
966
1447
|
`);
|
|
967
|
-
|
|
1448
|
+
process2.stdout.write(` handle: ${profile.handle} \xB7 harness: ${profile.harness}
|
|
968
1449
|
`);
|
|
969
|
-
|
|
1450
|
+
process2.stdout.write(
|
|
970
1451
|
` verification: ${profile.verified ? "verified (read-only OAuth)" : "self-reported"}
|
|
971
1452
|
`
|
|
972
1453
|
);
|
|
973
|
-
|
|
974
|
-
|
|
1454
|
+
process2.stdout.write("\n");
|
|
1455
|
+
process2.stdout.write(" \u2022 raw usage stays local \xB7 only league shared\n\n");
|
|
975
1456
|
return 0;
|
|
976
1457
|
}
|
|
977
|
-
async function cmdMatches() {
|
|
1458
|
+
async function cmdMatches(live) {
|
|
978
1459
|
const profile = loadProfile();
|
|
979
1460
|
if (!profile) {
|
|
980
|
-
|
|
1461
|
+
process2.stderr.write("Not connected yet. Run `vibedating connect` first.\n");
|
|
981
1462
|
return 1;
|
|
982
1463
|
}
|
|
1464
|
+
if (live && !canShareLive()) {
|
|
1465
|
+
process2.stderr.write(
|
|
1466
|
+
"Live matching is off (consent required). Opt in: `vibedating discover --live`.\n"
|
|
1467
|
+
);
|
|
1468
|
+
return 1;
|
|
1469
|
+
}
|
|
1470
|
+
const myIdx = leagueIndex(profile.league);
|
|
1471
|
+
const livePeers = canShareLive() ? loadPeers().filter((p) => {
|
|
1472
|
+
const idx = leagueIndex(p.league);
|
|
1473
|
+
return idx >= 0 && Math.abs(idx - myIdx) <= 1;
|
|
1474
|
+
}) : [];
|
|
1475
|
+
if (livePeers.length > 0) {
|
|
1476
|
+
process2.stdout.write(`Your league: ${leagueLabel(profile.league)}
|
|
1477
|
+
`);
|
|
1478
|
+
process2.stdout.write(
|
|
1479
|
+
`${livePeers.length} live peer${livePeers.length === 1 ? "" : "s"} in range (discovered over the DHT):
|
|
1480
|
+
|
|
1481
|
+
`
|
|
1482
|
+
);
|
|
1483
|
+
for (const p of livePeers) {
|
|
1484
|
+
process2.stdout.write(` ${p.handle.padEnd(28)} ${p.league} \xB7 ${p.harness}
|
|
1485
|
+
`);
|
|
1486
|
+
}
|
|
1487
|
+
process2.stdout.write("\n");
|
|
1488
|
+
return 0;
|
|
1489
|
+
}
|
|
1490
|
+
if (live) {
|
|
1491
|
+
process2.stdout.write("No live peers discovered yet. Run `vibedating discover` and keep it on.\n");
|
|
1492
|
+
return 0;
|
|
1493
|
+
}
|
|
983
1494
|
const list = matches(profile.league, CANDIDATES);
|
|
984
|
-
|
|
1495
|
+
process2.stdout.write(`Your league: ${leagueLabel(profile.league)}
|
|
985
1496
|
`);
|
|
986
|
-
|
|
1497
|
+
process2.stdout.write(`${list.length} candidate${list.length === 1 ? "" : "s"} in range (local demo pool):
|
|
987
1498
|
|
|
988
1499
|
`);
|
|
989
1500
|
if (list.length === 0) {
|
|
990
|
-
|
|
1501
|
+
process2.stdout.write(" (no candidates in range)\n");
|
|
991
1502
|
return 0;
|
|
992
1503
|
}
|
|
993
1504
|
for (const c of list) {
|
|
994
|
-
|
|
1505
|
+
process2.stdout.write(` ${c.handle.padEnd(28)} ${c.league}
|
|
995
1506
|
`);
|
|
996
1507
|
}
|
|
997
|
-
|
|
1508
|
+
process2.stdout.write("\n");
|
|
998
1509
|
return 0;
|
|
999
1510
|
}
|
|
1000
|
-
async function
|
|
1001
|
-
const
|
|
1002
|
-
|
|
1003
|
-
|
|
1511
|
+
async function cmdDiscover(live) {
|
|
1512
|
+
const profile = loadProfile();
|
|
1513
|
+
if (!profile) {
|
|
1514
|
+
process2.stderr.write("Not connected yet. Run `vibedating connect` first.\n");
|
|
1515
|
+
return 1;
|
|
1516
|
+
}
|
|
1517
|
+
if (live && !canShareLive()) grantLiveConsent();
|
|
1518
|
+
if (!canShareLive()) {
|
|
1519
|
+
process2.stderr.write(
|
|
1520
|
+
"Live discovery is off. It shares ONLY your handle + league + harness (never raw usage)\nwith same-league peers on the public DHT. Opt in: `vibedating discover --live`\n"
|
|
1521
|
+
);
|
|
1522
|
+
return 1;
|
|
1523
|
+
}
|
|
1524
|
+
const hello = {
|
|
1525
|
+
handle: profile.handle,
|
|
1526
|
+
league: profile.league,
|
|
1527
|
+
harness: profile.harness
|
|
1528
|
+
};
|
|
1529
|
+
process2.stdout.write("\n");
|
|
1530
|
+
process2.stdout.write(` ${LIVE_NOTICE}
|
|
1531
|
+
`);
|
|
1532
|
+
const session = await startDiscovery({
|
|
1533
|
+
hello,
|
|
1534
|
+
onPeer: (peer, isNew) => {
|
|
1535
|
+
process2.stdout.write(
|
|
1536
|
+
` + ${peer.handle} (${peer.league} \xB7 ${peer.harness})${isNew ? " \u2190 new match" : ""}
|
|
1537
|
+
`
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1540
|
+
});
|
|
1541
|
+
process2.stdout.write(
|
|
1542
|
+
` topic: ${TOPIC_PREFIX}${profile.league} \u2192 ${session.topic.toString("hex").slice(0, 12)}\u2026
|
|
1543
|
+
`
|
|
1544
|
+
);
|
|
1545
|
+
process2.stdout.write(" listening for same-league peers\u2026 (Ctrl+C to stop)\n\n");
|
|
1546
|
+
await new Promise((resolve) => {
|
|
1547
|
+
process2.once("SIGINT", () => resolve());
|
|
1548
|
+
process2.once("SIGTERM", () => resolve());
|
|
1549
|
+
});
|
|
1550
|
+
process2.stdout.write("\n leaving the swarm\u2026\n");
|
|
1551
|
+
await session.close();
|
|
1552
|
+
process2.stdout.write(
|
|
1553
|
+
` ${session.peers.size} peer${session.peers.size === 1 ? "" : "s"} discovered \xB7 saved to ~/.vibedating/peers.json
|
|
1004
1554
|
|
|
1555
|
+
`
|
|
1556
|
+
);
|
|
1557
|
+
return 0;
|
|
1558
|
+
}
|
|
1559
|
+
async function cmdOpen(port) {
|
|
1560
|
+
const profile = loadProfile();
|
|
1561
|
+
let live;
|
|
1562
|
+
let session;
|
|
1563
|
+
if (profile) {
|
|
1564
|
+
if (!canShareLive()) grantLiveConsent();
|
|
1565
|
+
live = createLiveBridge();
|
|
1566
|
+
const hello = {
|
|
1567
|
+
handle: profile.handle,
|
|
1568
|
+
league: profile.league,
|
|
1569
|
+
harness: profile.harness
|
|
1570
|
+
};
|
|
1571
|
+
process2.stdout.write(`
|
|
1572
|
+
${LIVE_NOTICE}
|
|
1573
|
+
`);
|
|
1574
|
+
session = await startDiscovery({ hello, onLink: (link) => live.addLink(link) });
|
|
1575
|
+
}
|
|
1576
|
+
const started = await startServer({ port, live });
|
|
1577
|
+
process2.stdout.write(`
|
|
1578
|
+
vibedating local web app \u2192 ${started.url}
|
|
1579
|
+
`);
|
|
1580
|
+
if (live) {
|
|
1581
|
+
process2.stdout.write(" \u2022 live A/V (video) available for connected same-league peers\n");
|
|
1582
|
+
} else {
|
|
1583
|
+
process2.stdout.write(" \u2022 connect first (`vibedating connect`) to enable live video\n");
|
|
1584
|
+
}
|
|
1585
|
+
process2.stdout.write(" \u2022 raw usage stays local \xB7 only league shared\n");
|
|
1586
|
+
process2.stdout.write(" (Ctrl+C to stop)\n\n");
|
|
1587
|
+
await new Promise((resolve) => {
|
|
1588
|
+
process2.once("SIGINT", () => resolve());
|
|
1589
|
+
process2.once("SIGTERM", () => resolve());
|
|
1590
|
+
});
|
|
1591
|
+
process2.stdout.write("\n shutting down\u2026\n");
|
|
1592
|
+
if (session) await session.close();
|
|
1593
|
+
await new Promise((resolve) => started.server.close(() => resolve()));
|
|
1594
|
+
return 0;
|
|
1595
|
+
}
|
|
1596
|
+
async function cmdLive(dating) {
|
|
1597
|
+
const profile = loadProfile();
|
|
1598
|
+
if (!profile) {
|
|
1599
|
+
process2.stderr.write("Not connected yet. Run `vibedating connect` first.\n");
|
|
1600
|
+
return 1;
|
|
1601
|
+
}
|
|
1602
|
+
if (!canShareLive()) grantLiveConsent();
|
|
1603
|
+
if (!canShareLive()) {
|
|
1604
|
+
process2.stderr.write("Could not enable live discovery. Try `vibedating discover --live`.\n");
|
|
1605
|
+
return 1;
|
|
1606
|
+
}
|
|
1607
|
+
const hello = {
|
|
1608
|
+
handle: profile.handle,
|
|
1609
|
+
league: profile.league,
|
|
1610
|
+
harness: profile.harness
|
|
1611
|
+
};
|
|
1612
|
+
process2.stdout.write("\n");
|
|
1613
|
+
process2.stdout.write(` ${LIVE_NOTICE}
|
|
1614
|
+
`);
|
|
1615
|
+
process2.stdout.write(
|
|
1616
|
+
dating ? " dating mode \u2014 /open <handle> to pick a peer\n" : " omegle mode \u2014 /next to roll a new peer\n"
|
|
1617
|
+
);
|
|
1618
|
+
const pairing = createPairing();
|
|
1619
|
+
pairing.onMatch((link) => {
|
|
1620
|
+
if (link === void 0) {
|
|
1621
|
+
process2.stdout.write(" \xB7 idle \u2014 no peer right now\n");
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
process2.stdout.write(
|
|
1625
|
+
` \xB7 matched ${link.hello.handle} (${link.hello.league} \xB7 ${link.hello.harness})
|
|
1626
|
+
`
|
|
1627
|
+
);
|
|
1628
|
+
link.onMessage((m) => {
|
|
1629
|
+
process2.stdout.write(` <${link.hello.handle}> ${m.text}
|
|
1005
1630
|
`);
|
|
1006
|
-
|
|
1007
|
-
|
|
1631
|
+
});
|
|
1632
|
+
});
|
|
1633
|
+
const session = await startDiscovery({
|
|
1634
|
+
hello,
|
|
1635
|
+
onLink: (link) => pairing.add(link)
|
|
1636
|
+
});
|
|
1637
|
+
process2.stdout.write(
|
|
1638
|
+
` topic: ${TOPIC_PREFIX}${profile.league} \u2192 ${session.topic.toString("hex").slice(0, 12)}\u2026
|
|
1639
|
+
`
|
|
1640
|
+
);
|
|
1641
|
+
process2.stdout.write(" type to chat \xB7 /next \xB7 /open <handle> \xB7 /quit\n");
|
|
1642
|
+
process2.stdout.write(" video chat: live A/V runs in the web app \u2014 run `vibedating open`\n\n");
|
|
1643
|
+
const rl = readline.createInterface({ input: process2.stdin, terminal: false });
|
|
1644
|
+
const stop = () => {
|
|
1645
|
+
rl.close();
|
|
1646
|
+
};
|
|
1647
|
+
process2.once("SIGINT", stop);
|
|
1648
|
+
process2.once("SIGTERM", stop);
|
|
1649
|
+
for await (const line of rl) {
|
|
1650
|
+
const text = line.trim();
|
|
1651
|
+
if (text === "/quit") break;
|
|
1652
|
+
if (text === "/next") {
|
|
1653
|
+
pairing.next();
|
|
1654
|
+
continue;
|
|
1655
|
+
}
|
|
1656
|
+
if (text.startsWith("/open ")) {
|
|
1657
|
+
const handle2 = text.slice("/open ".length).trim();
|
|
1658
|
+
if (pairing.open(handle2) === void 0) {
|
|
1659
|
+
process2.stdout.write(` \xB7 no available peer "${handle2}"
|
|
1660
|
+
`);
|
|
1661
|
+
}
|
|
1662
|
+
continue;
|
|
1663
|
+
}
|
|
1664
|
+
if (text === "") continue;
|
|
1665
|
+
const cur2 = pairing.current();
|
|
1666
|
+
if (cur2 !== void 0) {
|
|
1667
|
+
cur2.send(text);
|
|
1668
|
+
} else {
|
|
1669
|
+
process2.stdout.write(" \xB7 no peer yet \u2014 waiting for a match\u2026\n");
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
process2.removeListener("SIGINT", stop);
|
|
1673
|
+
process2.removeListener("SIGTERM", stop);
|
|
1674
|
+
const cur = pairing.current();
|
|
1675
|
+
if (cur !== void 0) cur.close();
|
|
1676
|
+
process2.stdout.write("\n leaving the swarm\u2026\n");
|
|
1677
|
+
await session.close();
|
|
1678
|
+
process2.stdout.write("\n");
|
|
1008
1679
|
return 0;
|
|
1009
1680
|
}
|
|
1010
1681
|
var HELP = `vibedating ${VERSION} \u2014 dating by tokens (local-first)
|
|
1011
1682
|
|
|
1012
1683
|
Usage:
|
|
1013
1684
|
vibedating connect Read your usage, compute + print your league
|
|
1014
|
-
vibedating matches
|
|
1685
|
+
vibedating matches [--live] List candidates in your league (live peers if any)
|
|
1686
|
+
vibedating discover [--live] Find live same-league peers over the DHT (opt-in)
|
|
1687
|
+
vibedating live [--dating] Live chat with same-league peers (omegle /next, or --dating pick)
|
|
1015
1688
|
vibedating open [--port N] Serve the local web app (default: random port)
|
|
1689
|
+
+ live A/V video with connected same-league peers
|
|
1016
1690
|
vibedating mcp Run the stdio MCP server (profile, matches)
|
|
1017
1691
|
vibedating --version
|
|
1018
1692
|
vibedating --help
|
|
1019
1693
|
|
|
1020
1694
|
Privacy:
|
|
1021
1695
|
Raw token usage is read and stored LOCALLY (~/.vibedating). Only the league
|
|
1022
|
-
bucket is ever shared.
|
|
1696
|
+
bucket is ever shared. Live discovery (off by default) shares ONLY your
|
|
1697
|
+
handle + league + harness with same-league peers \u2014 opt in with --live.
|
|
1023
1698
|
|
|
1024
1699
|
Env:
|
|
1025
1700
|
VIBEDATING_TOKENS=<n> Self-report a token count (e.g. 23400000 or 12M)
|
|
@@ -1030,42 +1705,46 @@ async function main(argv) {
|
|
|
1030
1705
|
const parsed = parseArgs(argv);
|
|
1031
1706
|
switch (parsed.command) {
|
|
1032
1707
|
case "version":
|
|
1033
|
-
|
|
1708
|
+
process2.stdout.write(`vibedating ${VERSION}
|
|
1034
1709
|
`);
|
|
1035
1710
|
return 0;
|
|
1036
1711
|
case "help":
|
|
1037
1712
|
case null:
|
|
1038
|
-
|
|
1713
|
+
process2.stdout.write(HELP);
|
|
1039
1714
|
return 0;
|
|
1040
1715
|
case "connect":
|
|
1041
1716
|
return cmdConnect();
|
|
1042
1717
|
case "matches":
|
|
1043
|
-
return cmdMatches();
|
|
1718
|
+
return cmdMatches(parsed.live);
|
|
1719
|
+
case "discover":
|
|
1720
|
+
return cmdDiscover(parsed.live);
|
|
1044
1721
|
case "open":
|
|
1045
1722
|
return cmdOpen(parsed.port);
|
|
1723
|
+
case "live":
|
|
1724
|
+
return cmdLive(parsed.dating);
|
|
1046
1725
|
case "mcp":
|
|
1047
1726
|
await runMcp();
|
|
1048
1727
|
return 0;
|
|
1049
1728
|
}
|
|
1050
1729
|
}
|
|
1051
|
-
var entryUrl =
|
|
1730
|
+
var entryUrl = process2.argv[1];
|
|
1052
1731
|
if (entryUrl !== void 0) {
|
|
1053
1732
|
let isMain = false;
|
|
1054
1733
|
try {
|
|
1055
|
-
isMain =
|
|
1734
|
+
isMain = true;
|
|
1056
1735
|
} catch {
|
|
1057
1736
|
isMain = false;
|
|
1058
1737
|
}
|
|
1059
1738
|
if (isMain) {
|
|
1060
|
-
void main(
|
|
1739
|
+
void main(process2.argv.slice(2)).then(
|
|
1061
1740
|
(code) => {
|
|
1062
|
-
if (code !== 0)
|
|
1741
|
+
if (code !== 0) process2.exit(code);
|
|
1063
1742
|
},
|
|
1064
1743
|
(err) => {
|
|
1065
|
-
|
|
1744
|
+
process2.stderr.write(err instanceof Error ? `${err.stack ?? err.message}
|
|
1066
1745
|
` : `${String(err)}
|
|
1067
1746
|
`);
|
|
1068
|
-
|
|
1747
|
+
process2.exit(1);
|
|
1069
1748
|
}
|
|
1070
1749
|
);
|
|
1071
1750
|
}
|