vibedate 0.8.3 → 0.8.6

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.
@@ -29,9 +29,10 @@ import {
29
29
  signHelloClaims,
30
30
  startDiscovery,
31
31
  startRoom
32
- } from "./chunk-3FJDVN72.js";
32
+ } from "./chunk-ZU5UPYSF.js";
33
33
 
34
34
  // src/mcp.ts
35
+ import { createRequire } from "module";
35
36
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
36
37
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
37
38
 
@@ -14890,6 +14891,7 @@ function uninstallDaemonService(opts) {
14890
14891
 
14891
14892
  // src/pairing.ts
14892
14893
  var MAX_BUFFERED = 100;
14894
+ var MAX_QUEUE = 100;
14893
14895
  function createPairing() {
14894
14896
  const queue = [];
14895
14897
  const matchCbs = /* @__PURE__ */ new Set();
@@ -14923,7 +14925,14 @@ function createPairing() {
14923
14925
  link.onClose(() => {
14924
14926
  buffers.delete(link);
14925
14927
  if (current === link) {
14926
- const nextUp = queue.shift();
14928
+ let nextUp;
14929
+ while (queue.length > 0) {
14930
+ const cand = queue.shift();
14931
+ if (!cand.closed) {
14932
+ nextUp = cand;
14933
+ break;
14934
+ }
14935
+ }
14927
14936
  setCurrent(nextUp);
14928
14937
  } else {
14929
14938
  const idx = queue.indexOf(link);
@@ -14980,6 +14989,10 @@ function createPairing() {
14980
14989
  setCurrent(link);
14981
14990
  } else {
14982
14991
  queue.push(link);
14992
+ if (queue.length > MAX_QUEUE) {
14993
+ const oldest = queue.shift();
14994
+ oldest.close();
14995
+ }
14983
14996
  }
14984
14997
  },
14985
14998
  next() {
@@ -14987,7 +15000,14 @@ function createPairing() {
14987
15000
  current.close();
14988
15001
  current = void 0;
14989
15002
  }
14990
- const nextUp = queue.shift();
15003
+ let nextUp;
15004
+ while (queue.length > 0) {
15005
+ const cand = queue.shift();
15006
+ if (!cand.closed) {
15007
+ nextUp = cand;
15008
+ break;
15009
+ }
15010
+ }
14991
15011
  setCurrent(nextUp);
14992
15012
  return current;
14993
15013
  },
@@ -15027,7 +15047,14 @@ function textBlock(text) {
15027
15047
  function jsonResult(value) {
15028
15048
  return { content: [textBlock(JSON.stringify(value, null, 2))] };
15029
15049
  }
15030
- var VERSION = "0.7.1";
15050
+ var VERSION = (() => {
15051
+ try {
15052
+ const require2 = createRequire(import.meta.url);
15053
+ return require2("../package.json").version ?? "0.0.0";
15054
+ } catch {
15055
+ return "0.0.0";
15056
+ }
15057
+ })();
15031
15058
  function discoveryScope(myLeague, any2) {
15032
15059
  const names = any2 ? allLeagueNames() : leaguesWithin(myLeague, 1);
15033
15060
  const ordered = [myLeague, ...names.filter((n) => n !== myLeague)];
@@ -18,13 +18,18 @@ import { makeEvent, notify as vibeCoreNotify } from "@pooriaarab/vibe-core";
18
18
  var MAX_TEXT_LEN = 4e3;
19
19
  var MAX_ID_LEN = 64;
20
20
  var MAX_B64_CHUNK_LEN = 16 * 1024;
21
+ var MAX_BINARY_CHUNK_BYTES = 64 * 1024;
21
22
  var MAX_MEDIA_SIZE = 25 * 1024 * 1024;
22
23
  var MAX_MIME_LEN = 128;
23
24
  var MAX_NAME_LEN = 256;
24
25
  var MAX_SDP_LEN = 64 * 1024;
25
26
  var MAX_CANDIDATE_LEN = 4 * 1024;
26
27
  var MAX_FRAME_LEN = Math.max(MAX_B64_CHUNK_LEN, 2 * MAX_SDP_LEN) + 2048;
28
+ var BIN_MEDIA_CHUNK_TAG = 1;
27
29
  function serializeFrame(f) {
30
+ if (f.t === "media-chunk" && "data" in f) {
31
+ throw new Error("binary media-chunk must use serializeBinaryMediaChunk, not serializeFrame");
32
+ }
28
33
  return JSON.stringify(f);
29
34
  }
30
35
  function parseFrame(raw) {
@@ -123,6 +128,107 @@ function parseFrame(raw) {
123
128
  return null;
124
129
  }
125
130
  }
131
+ function serializeBinaryMediaChunk(parts) {
132
+ const { id, seq, data } = parts;
133
+ if (typeof id !== "string" || id.length === 0 || id.length > MAX_ID_LEN) {
134
+ throw new Error(`invalid media chunk id length: ${typeof id === "string" ? id.length : "?"}`);
135
+ }
136
+ if (typeof seq !== "number" || !Number.isFinite(seq) || !Number.isInteger(seq) || seq < 0) {
137
+ throw new Error(`invalid media chunk seq: ${String(seq)}`);
138
+ }
139
+ if (!Buffer.isBuffer(data) || data.length === 0 || data.length > MAX_BINARY_CHUNK_BYTES) {
140
+ throw new Error(
141
+ `invalid media chunk payload length: ${Buffer.isBuffer(data) ? data.length : "n/a"}`
142
+ );
143
+ }
144
+ const hdr = Buffer.from(JSON.stringify({ id, seq }), "utf8");
145
+ if (hdr.length > 65535) throw new Error(`media chunk header too large: ${hdr.length}`);
146
+ const out = Buffer.allocUnsafe(1 + 2 + 4 + hdr.length + data.length);
147
+ let o = 0;
148
+ out[o++] = BIN_MEDIA_CHUNK_TAG;
149
+ out.writeUInt16BE(hdr.length, o);
150
+ o += 2;
151
+ out.writeUInt32BE(data.length, o);
152
+ o += 4;
153
+ hdr.copy(out, o);
154
+ o += hdr.length;
155
+ data.copy(out, o);
156
+ return out;
157
+ }
158
+ function tryParseBinaryMediaChunk(buf) {
159
+ if (buf.length === 0) return { frame: null, bytesConsumed: 0 };
160
+ if (buf[0] !== BIN_MEDIA_CHUNK_TAG) return { frame: null, bytesConsumed: 0 };
161
+ if (buf.length < 7) return { frame: null, bytesConsumed: 0 };
162
+ const hdrLen = buf.readUInt16BE(1);
163
+ const payloadLen = buf.readUInt32BE(3);
164
+ const MAX_HDR_LEN = 512;
165
+ if (hdrLen === 0 || hdrLen > MAX_HDR_LEN || payloadLen === 0 || payloadLen > MAX_BINARY_CHUNK_BYTES) {
166
+ return { frame: null, bytesConsumed: 1 };
167
+ }
168
+ const total = 7 + hdrLen + payloadLen;
169
+ if (buf.length < total) return { frame: null, bytesConsumed: 0 };
170
+ const hdrBuf = buf.subarray(7, 7 + hdrLen);
171
+ let hdrObj;
172
+ try {
173
+ hdrObj = JSON.parse(hdrBuf.toString("utf8"));
174
+ } catch {
175
+ return { frame: null, bytesConsumed: 1 };
176
+ }
177
+ if (typeof hdrObj !== "object" || hdrObj === null || Array.isArray(hdrObj)) {
178
+ return { frame: null, bytesConsumed: 1 };
179
+ }
180
+ const r = hdrObj;
181
+ const id = r["id"];
182
+ const seq = r["seq"];
183
+ if (typeof id !== "string" || id.length === 0 || id.length > MAX_ID_LEN) {
184
+ return { frame: null, bytesConsumed: 1 };
185
+ }
186
+ if (typeof seq !== "number" || !Number.isFinite(seq) || !Number.isInteger(seq) || seq < 0) {
187
+ return { frame: null, bytesConsumed: 1 };
188
+ }
189
+ const data = Buffer.from(buf.subarray(7 + hdrLen, 7 + hdrLen + payloadLen));
190
+ return {
191
+ frame: { t: "media-chunk", id, seq, data },
192
+ bytesConsumed: total
193
+ };
194
+ }
195
+ function pullFramesFromBuffer(buf) {
196
+ const frames = [];
197
+ let offset = 0;
198
+ while (offset < buf.length) {
199
+ const first = buf[offset];
200
+ if (first === BIN_MEDIA_CHUNK_TAG) {
201
+ const slice = offset === 0 ? buf : buf.subarray(offset);
202
+ const { frame: frame2, bytesConsumed } = tryParseBinaryMediaChunk(slice);
203
+ if (bytesConsumed === 0) {
204
+ break;
205
+ }
206
+ if (frame2 !== null) frames.push(frame2);
207
+ offset += bytesConsumed;
208
+ continue;
209
+ }
210
+ const nlRel = buf.indexOf(10, offset);
211
+ if (nlRel < 0) {
212
+ break;
213
+ }
214
+ const lineBuf = buf.subarray(offset, nlRel);
215
+ offset = nlRel + 1;
216
+ if (lineBuf.length === 0) continue;
217
+ let onlyWs = true;
218
+ for (let i = 0; i < lineBuf.length; i++) {
219
+ const c = lineBuf[i];
220
+ if (c !== 32 && c !== 9 && c !== 13) {
221
+ onlyWs = false;
222
+ break;
223
+ }
224
+ }
225
+ if (onlyWs) continue;
226
+ const frame = parseFrame(lineBuf);
227
+ if (frame !== null) frames.push(frame);
228
+ }
229
+ const rest = offset === 0 ? buf : buf.subarray(offset);
230
+ return { frames, rest: rest.length === 0 ? Buffer.alloc(0) : Buffer.from(rest) };
231
+ }
126
232
 
127
233
  // src/identity.ts
128
234
  import {
@@ -137,14 +243,20 @@ import { chmodSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writ
137
243
  import path2 from "path";
138
244
 
139
245
  // src/state.ts
140
- import { mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
246
+ import { mkdirSync, readFileSync, rmSync, renameSync, writeFileSync } from "fs";
141
247
  import os from "os";
142
248
  import path from "path";
143
249
  import { createConsentLedger } from "@pooriaarab/vibe-core";
250
+ function writeJsonAtomic(filePath, obj) {
251
+ const tmpPath = `${filePath}.tmp`;
252
+ writeFileSync(tmpPath, JSON.stringify(obj, null, 2) + "\n", "utf8");
253
+ renameSync(tmpPath, filePath);
254
+ }
144
255
  var CONSENT_SCOPE = "share:league";
145
256
  var LIVE_CONSENT_SCOPE = "share:live";
146
257
  function defaultStateDir() {
147
- return path.join(os.homedir(), ".vibedating");
258
+ const home = process.env["VIBEDATE_HOME"];
259
+ return home && home.length > 0 ? home : path.join(os.homedir(), ".vibedating");
148
260
  }
149
261
  var FileConsentStore = class {
150
262
  constructor(file) {
@@ -162,7 +274,7 @@ var FileConsentStore = class {
162
274
  }
163
275
  save(grants) {
164
276
  mkdirSync(path.dirname(this.file), { recursive: true });
165
- writeFileSync(this.file, JSON.stringify({ grants }, null, 2) + "\n", "utf8");
277
+ writeJsonAtomic(this.file, { grants });
166
278
  }
167
279
  };
168
280
  function createLedger(dir = defaultStateDir()) {
@@ -184,7 +296,7 @@ function connectProfile(snapshot, handle, dir = defaultStateDir()) {
184
296
  connectedAt: (/* @__PURE__ */ new Date()).toISOString()
185
297
  };
186
298
  mkdirSync(dir, { recursive: true });
187
- writeFileSync(profilePath(dir), JSON.stringify(state, null, 2) + "\n", "utf8");
299
+ writeJsonAtomic(profilePath(dir), state);
188
300
  return state;
189
301
  }
190
302
  function loadProfile(dir = defaultStateDir()) {
@@ -245,14 +357,10 @@ function saveHandle(handle, dir = defaultStateDir()) {
245
357
  );
246
358
  }
247
359
  mkdirSync(dir, { recursive: true });
248
- writeFileSync(handleFilePath(dir), JSON.stringify({ handle: canonical }, null, 2) + "\n", "utf8");
360
+ writeJsonAtomic(handleFilePath(dir), { handle: canonical });
249
361
  const existing = loadProfile(dir);
250
362
  if (existing !== null) {
251
- writeFileSync(
252
- profilePath(dir),
253
- JSON.stringify({ ...existing, handle: canonical }, null, 2) + "\n",
254
- "utf8"
255
- );
363
+ writeJsonAtomic(profilePath(dir), { ...existing, handle: canonical });
256
364
  }
257
365
  return canonical;
258
366
  }
@@ -270,7 +378,7 @@ function blocklistPath(dir) {
270
378
  }
271
379
  function persistBlocklist(blocked, dir) {
272
380
  mkdirSync(dir, { recursive: true });
273
- writeFileSync(blocklistPath(dir), JSON.stringify({ blocked }, null, 2) + "\n", "utf8");
381
+ writeJsonAtomic(blocklistPath(dir), { blocked });
274
382
  }
275
383
  function loadBlocklist(dir = defaultStateDir()) {
276
384
  try {
@@ -442,7 +550,7 @@ import { readFile } from "fs/promises";
442
550
  import { writeFileSync as writeFileSync3 } from "fs";
443
551
  import os2 from "os";
444
552
  import path3 from "path";
445
- var DEFAULT_CHUNK_BYTES = Math.floor(MAX_B64_CHUNK_LEN * 3 / 4);
553
+ var DEFAULT_CHUNK_BYTES = MAX_BINARY_CHUNK_BYTES;
446
554
  var MIME_BY_EXT = {
447
555
  ".png": "image/png",
448
556
  ".jpg": "image/jpeg",
@@ -455,11 +563,32 @@ var MIME_BY_EXT = {
455
563
  function inferMime(name) {
456
564
  return MIME_BY_EXT[path3.extname(name).toLowerCase()] ?? "application/octet-stream";
457
565
  }
458
- function writeFrame(socket, line) {
459
- return new Promise((resolve) => {
460
- const ok = socket.write(line);
566
+ function writeBytes(socket, data) {
567
+ return new Promise((resolve, reject) => {
568
+ const ok = socket.write(data);
461
569
  if (ok) resolve();
462
- else socket.once("drain", () => resolve());
570
+ else {
571
+ const onDrain = () => {
572
+ cleanup();
573
+ resolve();
574
+ };
575
+ const onClose = () => {
576
+ cleanup();
577
+ reject(new Error("Socket closed before drain"));
578
+ };
579
+ const onError = (err) => {
580
+ cleanup();
581
+ reject(err);
582
+ };
583
+ const cleanup = () => {
584
+ socket.removeListener("drain", onDrain);
585
+ socket.removeListener("close", onClose);
586
+ socket.removeListener("error", onError);
587
+ };
588
+ socket.once("drain", onDrain);
589
+ socket.once("close", onClose);
590
+ socket.once("error", onError);
591
+ }
463
592
  });
464
593
  }
465
594
  async function sendMedia(opts) {
@@ -471,15 +600,23 @@ async function sendMedia(opts) {
471
600
  }
472
601
  if (mime.length > MAX_MIME_LEN) throw new Error(`mime too long: ${mime.length} > ${MAX_MIME_LEN}`);
473
602
  if (name.length > MAX_NAME_LEN) throw new Error(`name too long: ${name.length} > ${MAX_NAME_LEN}`);
474
- await writeFrame(socket, serializeFrame({ t: "media-start", id, mime, size, name }) + "\n");
475
- const chunkBytes = opts.chunkBytes ?? DEFAULT_CHUNK_BYTES;
603
+ await writeBytes(socket, serializeFrame({ t: "media-start", id, mime, size, name }) + "\n");
604
+ const chunkBytes = Math.min(
605
+ opts.chunkBytes ?? DEFAULT_CHUNK_BYTES,
606
+ opts.legacyJson ? Math.floor(16 * 1024 * 3 / 4) : MAX_BINARY_CHUNK_BYTES
607
+ );
476
608
  let seq = 0;
477
609
  for (let off = 0; off < size; off += chunkBytes) {
478
- const b64 = data.subarray(off, off + chunkBytes).toString("base64");
479
- await writeFrame(socket, serializeFrame({ t: "media-chunk", id, seq, b64 }) + "\n");
610
+ const slice = data.subarray(off, off + chunkBytes);
611
+ if (opts.legacyJson) {
612
+ const b64 = slice.toString("base64");
613
+ await writeBytes(socket, serializeFrame({ t: "media-chunk", id, seq, b64 }) + "\n");
614
+ } else {
615
+ await writeBytes(socket, serializeBinaryMediaChunk({ id, seq, data: slice }));
616
+ }
480
617
  seq++;
481
618
  }
482
- await writeFrame(socket, serializeFrame({ t: "media-end", id }) + "\n");
619
+ await writeBytes(socket, serializeFrame({ t: "media-end", id }) + "\n");
483
620
  return { id, size };
484
621
  }
485
622
  async function sendMediaFile(opts) {
@@ -492,7 +629,8 @@ async function sendMediaFile(opts) {
492
629
  mime,
493
630
  name,
494
631
  id: opts.id,
495
- chunkBytes: opts.chunkBytes
632
+ chunkBytes: opts.chunkBytes,
633
+ legacyJson: opts.legacyJson
496
634
  });
497
635
  }
498
636
  function safeName(id, name) {
@@ -533,9 +671,20 @@ var MediaReceiver = class {
533
671
  return;
534
672
  }
535
673
  let bytes;
536
- try {
537
- bytes = Buffer.from(frame.b64, "base64");
538
- } catch {
674
+ if ("data" in frame && Buffer.isBuffer(frame.data)) {
675
+ bytes = frame.data;
676
+ if (bytes.length === 0 || bytes.length > MAX_BINARY_CHUNK_BYTES) {
677
+ this.abort(frame.id);
678
+ return;
679
+ }
680
+ } else if ("b64" in frame && typeof frame.b64 === "string") {
681
+ try {
682
+ bytes = Buffer.from(frame.b64, "base64");
683
+ } catch {
684
+ this.abort(frame.id);
685
+ return;
686
+ }
687
+ } else {
539
688
  this.abort(frame.id);
540
689
  return;
541
690
  }
@@ -558,7 +707,9 @@ var MediaReceiver = class {
558
707
  const filePath = path3.join(this.opts.tmpDir ?? os2.tmpdir(), safeName(frame.id, tx.name));
559
708
  try {
560
709
  writeFileSync3(filePath, buf);
561
- } catch {
710
+ } catch (err) {
711
+ const error = err instanceof Error ? err : new Error(String(err));
712
+ this.onMedia({ mime: tx.mime, name: tx.name, path: filePath, size: tx.received, error });
562
713
  return;
563
714
  }
564
715
  this.onMedia({ mime: tx.mime, name: tx.name, path: filePath, size: tx.received });
@@ -576,7 +727,7 @@ function createPeerLink(socket, hello, initialBuffer = "", linkOpts = {}) {
576
727
  const mediaCbs = /* @__PURE__ */ new Set();
577
728
  const signalCbs = /* @__PURE__ */ new Set();
578
729
  const closeCbs = /* @__PURE__ */ new Set();
579
- let buf = initialBuffer;
730
+ let buf = typeof initialBuffer === "string" ? Buffer.from(initialBuffer, "utf8") : Buffer.from(initialBuffer);
580
731
  let closed = false;
581
732
  let mediaReceiver;
582
733
  const ensureMediaReceiver = () => {
@@ -624,19 +775,15 @@ function createPeerLink(socket, hello, initialBuffer = "", linkOpts = {}) {
624
775
  }
625
776
  };
626
777
  const pump = () => {
627
- let nl;
628
- while ((nl = buf.indexOf("\n")) >= 0) {
629
- const line = buf.slice(0, nl);
630
- buf = buf.slice(nl + 1);
631
- if (line.trim() === "") continue;
632
- const frame = parseFrame(line);
633
- if (frame === null) continue;
634
- dispatch(frame);
635
- }
778
+ if (buf.length === 0) return;
779
+ const { frames, rest } = pullFramesFromBuffer(buf);
780
+ buf = rest;
781
+ for (const frame of frames) dispatch(frame);
636
782
  };
637
783
  pump();
638
784
  socket.on("data", (chunk) => {
639
- buf += chunk.toString("utf8");
785
+ const bytes = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
786
+ buf = buf.length === 0 ? Buffer.from(bytes) : Buffer.concat([buf, bytes]);
640
787
  pump();
641
788
  });
642
789
  socket.on("end", () => {
@@ -659,6 +806,9 @@ function createPeerLink(socket, hello, initialBuffer = "", linkOpts = {}) {
659
806
  });
660
807
  return {
661
808
  hello,
809
+ get closed() {
810
+ return closed;
811
+ },
662
812
  send(text) {
663
813
  if (closed) return;
664
814
  const frame = { t: "msg", id: randomUUID2(), text, at: Date.now() };
@@ -846,7 +996,8 @@ async function startDiscovery(opts) {
846
996
  const topics = opts.topics ? [...opts.topics] : opts.topic !== void 0 ? [opts.topic] : [leagueTopic(hello.league)];
847
997
  const acceptLeague = opts.acceptLeague ?? ((l) => l === hello.league);
848
998
  const { default: Hyperswarm } = await import("hyperswarm");
849
- const swarm = new Hyperswarm(opts.bootstrap === void 0 ? {} : { bootstrap: opts.bootstrap });
999
+ const bootstrap = opts.bootstrap ?? parseBootstrapEnv(process.env["VIBEDATE_BOOTSTRAP"]);
1000
+ const swarm = new Hyperswarm(bootstrap === void 0 ? {} : { bootstrap });
850
1001
  await swarm.dht.fullyBootstrapped();
851
1002
  const peers = /* @__PURE__ */ new Map();
852
1003
  swarm.on("connection", (socket, info) => {
@@ -863,15 +1014,18 @@ async function startDiscovery(opts) {
863
1014
  ...hello.sig !== void 0 ? { sig: hello.sig } : {}
864
1015
  }) + "\n"
865
1016
  );
866
- let buf = "";
1017
+ let buf = Buffer.alloc(0);
867
1018
  let handedOff = false;
868
1019
  const onData = (chunk) => {
869
1020
  if (handedOff) return;
870
- buf += chunk.toString("utf8");
1021
+ buf = buf.length === 0 ? Buffer.from(chunk) : Buffer.concat([buf, chunk]);
871
1022
  let nl;
872
- while ((nl = buf.indexOf("\n")) >= 0) {
873
- const line = buf.slice(0, nl);
874
- buf = buf.slice(nl + 1);
1023
+ while ((nl = buf.indexOf(
1024
+ 10
1025
+ /* \n */
1026
+ )) >= 0) {
1027
+ const line = buf.subarray(0, nl).toString("utf8");
1028
+ buf = buf.subarray(nl + 1);
875
1029
  if (line.trim() === "") continue;
876
1030
  const frame = parseFrame(line);
877
1031
  if (frame === null) continue;
@@ -917,7 +1071,7 @@ async function startDiscovery(opts) {
917
1071
  });
918
1072
  onLink(link);
919
1073
  }
920
- buf = "";
1074
+ buf = Buffer.alloc(0);
921
1075
  return;
922
1076
  }
923
1077
  };
@@ -958,6 +1112,21 @@ async function startDiscovery(opts) {
958
1112
  }
959
1113
  };
960
1114
  }
1115
+ function parseBootstrapEnv(raw) {
1116
+ if (raw === void 0 || raw.trim() === "") return void 0;
1117
+ const nodes = [];
1118
+ for (const entry of raw.split(",")) {
1119
+ const trimmed = entry.trim();
1120
+ if (trimmed === "") continue;
1121
+ const idx = trimmed.lastIndexOf(":");
1122
+ if (idx <= 0) continue;
1123
+ const host = trimmed.slice(0, idx);
1124
+ const port = Number(trimmed.slice(idx + 1));
1125
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) continue;
1126
+ nodes.push({ host, port });
1127
+ }
1128
+ return nodes.length > 0 ? nodes : void 0;
1129
+ }
961
1130
 
962
1131
  // src/relay.ts
963
1132
  import { createHash as createHash2 } from "crypto";
@@ -1067,6 +1236,14 @@ async function createNostrRelayLink(opts) {
1067
1236
  return;
1068
1237
  }
1069
1238
  peerNostrPubkey = event.pubkey;
1239
+ try {
1240
+ const parsed = JSON.parse(plaintext);
1241
+ if (parsed && typeof parsed === "object" && "__vibe_rtc" in parsed) {
1242
+ for (const cb of signalCbs) cb(parsed.__vibe_rtc);
1243
+ return;
1244
+ }
1245
+ } catch {
1246
+ }
1070
1247
  const msg = { id: event.id, text: plaintext, at: event.created_at * 1e3 };
1071
1248
  for (const cb of messageCbs) cb(msg);
1072
1249
  return;
@@ -1075,6 +1252,9 @@ async function createNostrRelayLink(opts) {
1075
1252
  publishPresence();
1076
1253
  return {
1077
1254
  hello,
1255
+ get closed() {
1256
+ return closed;
1257
+ },
1078
1258
  send(text) {
1079
1259
  if (closed) return;
1080
1260
  if (peerNostrPubkey === void 0) {
@@ -1088,9 +1268,14 @@ async function createNostrRelayLink(opts) {
1088
1268
  async sendMedia() {
1089
1269
  return { id: "", size: 0 };
1090
1270
  },
1091
- // ponytail: relay WebRTC signaling (rtc-offer/answer/ice as kind-4 payloads)
1092
- // so A/V could also fall back through the relay. v0: text only.
1093
- sendSignal() {
1271
+ sendSignal(frame) {
1272
+ if (closed) return;
1273
+ const payload = JSON.stringify({ __vibe_rtc: frame });
1274
+ if (peerNostrPubkey === void 0) {
1275
+ pending.push(payload);
1276
+ return;
1277
+ }
1278
+ sendEncrypted(payload, peerNostrPubkey);
1094
1279
  },
1095
1280
  onMessage(cb) {
1096
1281
  messageCbs.add(cb);
@@ -1163,6 +1348,7 @@ var ROOM_TOPIC_PREFIX = "vibedate-room:";
1163
1348
  function roomTopic(name) {
1164
1349
  return createHash3("sha256").update(`${ROOM_TOPIC_PREFIX}${name}`, "utf8").digest();
1165
1350
  }
1351
+ var MAX_ROOM = 32;
1166
1352
  async function startRoom(opts) {
1167
1353
  const topic = roomTopic(opts.room);
1168
1354
  const entries = /* @__PURE__ */ new Map();
@@ -1186,7 +1372,19 @@ async function startRoom(opts) {
1186
1372
  ...opts.stateDir === void 0 ? {} : { stateDir: opts.stateDir },
1187
1373
  ...opts.notify === void 0 ? {} : { notify: opts.notify },
1188
1374
  onLink: (link) => {
1375
+ const isSelf = link.hello.pubkey !== void 0 && opts.hello.pubkey !== void 0 ? link.hello.pubkey === opts.hello.pubkey : link.hello.handle === opts.hello.handle;
1376
+ if (isSelf) {
1377
+ link.close();
1378
+ return;
1379
+ }
1189
1380
  const handle = link.hello.handle;
1381
+ if (!entries.has(handle) && entries.size >= MAX_ROOM) {
1382
+ link.send(`Room is full (${MAX_ROOM} members max). Try again later.`);
1383
+ link.close();
1384
+ return;
1385
+ }
1386
+ const cur = entries.get(handle);
1387
+ if (cur) cur.link.close();
1190
1388
  entries.set(handle, { hello: link.hello, link });
1191
1389
  memberHellos.set(handle, link.hello);
1192
1390
  fireRoster();
@@ -1197,8 +1395,8 @@ async function startRoom(opts) {
1197
1395
  for (const cb of signalCbs) cb(handle, frame);
1198
1396
  });
1199
1397
  link.onClose(() => {
1200
- const cur = entries.get(handle);
1201
- if (cur && cur.link === link) {
1398
+ const cur2 = entries.get(handle);
1399
+ if (cur2 && cur2.link === link) {
1202
1400
  entries.delete(handle);
1203
1401
  memberHellos.delete(handle);
1204
1402
  fireRoster();
package/dist/cli.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  stopDaemon,
11
11
  uninstallDaemonService,
12
12
  writeDaemonState
13
- } from "./chunk-XUWNLMOY.js";
13
+ } from "./chunk-NSSNOQJG.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-3FJDVN72.js";
49
+ } from "./chunk-ZU5UPYSF.js";
50
50
 
51
51
  // src/cli.ts
52
52
  import readline from "readline";
@@ -1389,9 +1389,8 @@ var webAppHtml = `<!DOCTYPE html>
1389
1389
  var idleIdx = 0;
1390
1390
 
1391
1391
  function rtcConfig(){
1392
- // A public STUN server crosses most NATs. No TURN in v0 \u2014 symmetric NATs
1393
- // won't connect, but signaling still completes and the call fails open.
1394
- return { iceServers: [{ urls: "stun:stun.l.google.com:19302" }] };
1392
+ var extra = (window.__VIBE_ICE__ && window.__VIBE_ICE__.iceServers) || [];
1393
+ return { iceServers: [{ urls: "stun:stun.l.google.com:19302" }].concat(extra) };
1395
1394
  }
1396
1395
  function sleep(ms){ return new Promise(function(r){ setTimeout(r, ms); }); }
1397
1396
 
@@ -2228,10 +2227,11 @@ function createLiveBridge() {
2228
2227
  link.onMedia((m) => {
2229
2228
  const mb = boxes.get(handle2);
2230
2229
  if (!mb) {
2231
- fs.unlink(m.path, () => {
2230
+ if (!m.error) fs.unlink(m.path, () => {
2232
2231
  });
2233
2232
  return;
2234
2233
  }
2234
+ if (m.error) return;
2235
2235
  mb.media.push({ ...m, name: sanitizePeerText(m.name), mime: sanitizePeerText(m.mime) });
2236
2236
  if (mb.media.length > MAX_QUEUED_MESSAGES) {
2237
2237
  const dropped = mb.media.splice(0, mb.media.length - MAX_QUEUED_MESSAGES);
@@ -2459,7 +2459,18 @@ async function handle(req, res, opts) {
2459
2459
  const url = new URL(req.url ?? "/", "http://localhost");
2460
2460
  const pathname = url.pathname;
2461
2461
  if (req.method === "GET" && pathname === "/") {
2462
- send(res, 200, "text/html; charset=utf-8", webAppHtml);
2462
+ const turnUrl = process.env["VIBEDATE_TURN_URL"];
2463
+ let html = webAppHtml;
2464
+ if (turnUrl) {
2465
+ const turnUser = process.env["VIBEDATE_TURN_USER"];
2466
+ const turnCred = process.env["VIBEDATE_TURN_CRED"];
2467
+ const server = { urls: turnUrl };
2468
+ if (turnUser) server.username = turnUser;
2469
+ if (turnCred) server.credential = turnCred;
2470
+ const script = `<script>window.__VIBE_ICE__={iceServers:${JSON.stringify([server]).replace(/</g, "\\u003c")}};</script>`;
2471
+ html = html.replace("<script>", script + "\n<script>");
2472
+ }
2473
+ send(res, 200, "text/html; charset=utf-8", html);
2463
2474
  return;
2464
2475
  }
2465
2476
  if (req.method === "GET" && pathname === "/api/state") {
@@ -2784,7 +2795,7 @@ async function handle(req, res, opts) {
2784
2795
  }
2785
2796
 
2786
2797
  // src/cli.ts
2787
- var VERSION = "0.8.3";
2798
+ var VERSION = "0.8.6";
2788
2799
  function parsePort(raw) {
2789
2800
  const n = Number(raw);
2790
2801
  if (!Number.isInteger(n) || n < 1 || n > 65535) return void 0;
@@ -3136,7 +3147,7 @@ async function cmdDiscover(live, any, viaRelay) {
3136
3147
  );
3137
3148
  return 0;
3138
3149
  }
3139
- async function cmdOpen(port, any, room) {
3150
+ async function cmdOpen(port, any, room, viaRelay, to) {
3140
3151
  const profile = loadProfile();
3141
3152
  let live;
3142
3153
  let roomBridge;
@@ -3167,17 +3178,45 @@ async function cmdOpen(port, any, room) {
3167
3178
  }).catch(() => {
3168
3179
  });
3169
3180
  } else if (live) {
3170
- const { topics, acceptLeague } = discoveryScope(profile.league, any);
3171
- void startDiscovery({
3172
- hello,
3173
- topics,
3174
- acceptLeague,
3175
- isBlocked: blockedChecker(),
3176
- onLink: (link) => live.addLink(link)
3177
- }).then((s) => {
3178
- session = s;
3179
- }).catch(() => {
3180
- });
3181
+ if (viaRelay && to !== void 0) {
3182
+ const target = normalizeHandle(to);
3183
+ const peer = target ? loadPeers().find((p) => sameHandle(p.handle, target) && typeof p.pubkey === "string") : void 0;
3184
+ if (peer && peer.pubkey) {
3185
+ const peerPubkey = peer.pubkey;
3186
+ void (async () => {
3187
+ try {
3188
+ const myNostr = await loadOrCreateNostrKey();
3189
+ const transport = await createNostrPoolTransport();
3190
+ const identity = loadOrCreateIdentity();
3191
+ const link = await createNostrRelayLink({
3192
+ myNostr,
3193
+ myEd25519Hex: identity.publicKeyHex,
3194
+ peerEd25519Hex: peerPubkey,
3195
+ hello: peer,
3196
+ transport
3197
+ });
3198
+ live.addLink(link);
3199
+ } catch {
3200
+ }
3201
+ })();
3202
+ } else {
3203
+ process2.stderr.write(`
3204
+ warning: --via-relay peer ${to} not found or lacks identity pubkey.
3205
+ `);
3206
+ }
3207
+ } else {
3208
+ const { topics, acceptLeague } = discoveryScope(profile.league, any);
3209
+ void startDiscovery({
3210
+ hello,
3211
+ topics,
3212
+ acceptLeague,
3213
+ isBlocked: blockedChecker(),
3214
+ onLink: (link) => live.addLink(link)
3215
+ }).then((s) => {
3216
+ session = s;
3217
+ }).catch(() => {
3218
+ });
3219
+ }
3181
3220
  }
3182
3221
  }
3183
3222
  process2.stdout.write(`
@@ -3390,10 +3429,17 @@ async function cmdLive(dating, any, to, keepAlive, viaRelay) {
3390
3429
  `);
3391
3430
  }
3392
3431
  link.onMedia((m) => {
3393
- process2.stdout.write(
3394
- ` \u{1F4CE} <${sanitizePeerText(link.hello.handle)}> sent ${sanitizePeerText(m.name)} \u2014 saved to ${m.path}
3432
+ if (m.error) {
3433
+ process2.stdout.write(
3434
+ ` \u{1F4CE} <${sanitizePeerText(link.hello.handle)}> sent ${sanitizePeerText(m.name)} \u2014 FAILED to save: ${m.error.message}
3395
3435
  `
3396
- );
3436
+ );
3437
+ } else {
3438
+ process2.stdout.write(
3439
+ ` \u{1F4CE} <${sanitizePeerText(link.hello.handle)}> sent ${sanitizePeerText(m.name)} \u2014 saved to ${m.path}
3440
+ `
3441
+ );
3442
+ }
3397
3443
  });
3398
3444
  pairing.add(link);
3399
3445
  }
@@ -3884,7 +3930,7 @@ async function main(argv) {
3884
3930
  case "discover":
3885
3931
  return cmdDiscover(parsed.live, parsed.any, parsed.viaRelay);
3886
3932
  case "open":
3887
- return cmdOpen(parsed.port, parsed.any, parsed.room);
3933
+ return cmdOpen(parsed.port, parsed.any, parsed.room, parsed.viaRelay, parsed.to);
3888
3934
  case "live":
3889
3935
  return cmdLive(parsed.dating, parsed.any, parsed.to, parsed.keepAlive, parsed.viaRelay);
3890
3936
  case "find":
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-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';
3
+ import { a as PeerHello, P as PeerLink } from './room-DgdlQcRD.js';
4
+ export { c as DiscoveryOptions, D as DiscoverySession, L as LIVE_NOTICE, d as ROOM_TOPIC_PREFIX, e as RoomMember, b as RoomMessage, f as RoomOptions, R 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-DgdlQcRD.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-3FJDVN72.js";
39
+ } from "./chunk-ZU5UPYSF.js";
40
40
  export {
41
41
  BELOW_LEAGUE,
42
42
  CANDIDATES,
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ runMcp
4
+ } from "./chunk-NSSNOQJG.js";
5
+ import "./chunk-ZU5UPYSF.js";
6
+
7
+ // src/mcp-bin.ts
8
+ runMcp().catch((err) => {
9
+ process.stderr.write(
10
+ `vibedate-mcp: ${err instanceof Error ? err.stack ?? err.message : String(err)}
11
+ `
12
+ );
13
+ process.exit(1);
14
+ });
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-DpZhNvuu.js';
2
+ import { P as PeerLink, a as PeerHello, R as RoomSession, D as DiscoverySession, b as RoomMessage } from './room-DgdlQcRD.js';
3
3
  import '@pooriaarab/vibe-core';
4
4
 
5
5
  /**
@@ -86,36 +86,6 @@ interface LivePairing {
86
86
  queued(): PeerLink[];
87
87
  }
88
88
 
89
- /**
90
- * vibedating MCP server (stdio) — full agent-native tool surface.
91
- *
92
- * An agent drives vibedate ENTIRELY via tool calls — no interactive terminal.
93
- * This is the fix for agents whose interactive `live` sessions time out.
94
- *
95
- * Tools (every response is structured JSON with `{ ok, ... }` / `{ ok:false, error }`):
96
- *
97
- * READ/STATE
98
- * get_profile · connect · matches · handle_get · handle_set · blocklist
99
- * block · unblock · daemon_status
100
- *
101
- * DISCOVERY
102
- * discover · who · find
103
- *
104
- * LIVE CHAT (stateful in-process session — poll, don't block a TTY)
105
- * live_start · live_peers · live_send · live_poll · live_open · live_next · live_stop
106
- *
107
- * ROOMS
108
- * room_join · room_send · room_poll · room_who · room_leave
109
- *
110
- * MEDIA
111
- * media_send
112
- *
113
- * Legacy aliases `profile` / `matches` remain registered for back-compat.
114
- *
115
- * AEGIS: peer text is UNTRUSTED display data — sanitized before return, never
116
- * executed, never fed to a shell.
117
- */
118
-
119
89
  /** Injectable hooks so tests can drive live/room without the real DHT. */
120
90
  interface McpLiveHooks {
121
91
  /** Build (or inject) the pairing object used by live_* tools. */
package/dist/mcp.js CHANGED
@@ -3,8 +3,8 @@ import {
3
3
  createMcpServer,
4
4
  createSessionState,
5
5
  runMcp
6
- } from "./chunk-XUWNLMOY.js";
7
- import "./chunk-3FJDVN72.js";
6
+ } from "./chunk-NSSNOQJG.js";
7
+ import "./chunk-ZU5UPYSF.js";
8
8
  export {
9
9
  MCP_TOOL_NAMES,
10
10
  createMcpServer,
@@ -24,11 +24,20 @@ type Frame = {
24
24
  mime: string;
25
25
  size: number;
26
26
  name: string;
27
- } | {
27
+ }
28
+ /** Legacy JSON media-chunk (base64). Prefer binary wire for new sends. */
29
+ | {
28
30
  t: 'media-chunk';
29
31
  id: string;
30
32
  seq: number;
31
33
  b64: string;
34
+ }
35
+ /** Binary-path media-chunk: raw bytes, no base64. Produced by the binary framer. */
36
+ | {
37
+ t: 'media-chunk';
38
+ id: string;
39
+ seq: number;
40
+ data: Buffer;
32
41
  } | {
33
42
  t: 'media-end';
34
43
  id: string;
@@ -56,11 +65,14 @@ interface ReceivedMedia {
56
65
  /** Temp file path holding the reassembled bytes. */
57
66
  readonly path: string;
58
67
  readonly size: number;
68
+ readonly error?: Error;
59
69
  }
60
70
 
61
71
  interface PeerLink {
62
72
  /** The validated identity of the remote peer (from the hello handshake). */
63
73
  readonly hello: PeerHello;
74
+ /** Whether the link has been closed. */
75
+ readonly closed: boolean;
64
76
  /** Send a line of text as a `msg` frame. */
65
77
  send(text: string): void;
66
78
  /** Read a file from disk and send it as a chunked media transfer. */
@@ -317,4 +329,4 @@ interface RoomSession {
317
329
  */
318
330
  declare function startRoom(opts: RoomOptions): Promise<RoomSession>;
319
331
 
320
- export { type DiscoveryOptions as D, LIVE_NOTICE as L, type PeerHello as P, ROOM_TOPIC_PREFIX as R, type StoredPeer as S, TOPIC_PREFIX as T, type PeerLink as a, type DiscoverySession as b, type RoomMember as c, type RoomMessage as d, type RoomOptions as e, type RoomSession as f, loadPeers as g, recordPeerMessage as h, roomTopic as i, startDiscovery as j, startRoom as k, leagueTopic as l, parseHandshake as p, recordPeer as r, serializeHandshake as s };
332
+ export { type DiscoverySession as D, LIVE_NOTICE as L, type PeerLink as P, type RoomSession as R, type StoredPeer as S, TOPIC_PREFIX as T, type PeerHello as a, type RoomMessage as b, type DiscoveryOptions as c, ROOM_TOPIC_PREFIX as d, type RoomMember as e, type RoomOptions as f, loadPeers as g, recordPeerMessage as h, roomTopic as i, startDiscovery as j, startRoom as k, leagueTopic as l, parseHandshake as p, recordPeer as r, serializeHandshake as s };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibedate",
3
- "version": "0.8.3",
3
+ "version": "0.8.6",
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",
@@ -8,7 +8,7 @@
8
8
  "bin": {
9
9
  "vibedate": "./dist/cli.js",
10
10
  "vibedating": "./dist/cli.js",
11
- "vibedate-mcp": "./dist/mcp.js"
11
+ "vibedate-mcp": "./dist/mcp-bin.js"
12
12
  },
13
13
  "main": "./dist/index.js",
14
14
  "types": "./dist/index.d.ts",
@@ -22,10 +22,12 @@
22
22
  "dist"
23
23
  ],
24
24
  "scripts": {
25
- "build": "tsup src/index.ts src/cli.ts src/mcp.ts --format esm --dts --clean",
25
+ "build": "tsup src/index.ts src/cli.ts src/mcp.ts src/mcp-bin.ts --format esm --dts --clean",
26
26
  "typecheck": "tsc --noEmit",
27
- "test": "vitest run",
28
- "test:watch": "vitest"
27
+ "test": "vitest run --exclude '**/*.harness.test.ts'",
28
+ "test:watch": "vitest",
29
+ "bench": "tsx bench/run.ts",
30
+ "test:harness": "npm run build && vitest run harness.test"
29
31
  },
30
32
  "dependencies": {
31
33
  "@modelcontextprotocol/sdk": "^1.0.0",
@@ -37,6 +39,7 @@
37
39
  "@types/node": "^20.11.0",
38
40
  "hyperdht": "^6.33.0",
39
41
  "tsup": "^8.0.0",
42
+ "tsx": "^4.23.1",
40
43
  "typescript": "^5.4.0",
41
44
  "vitest": "^1.6.0",
42
45
  "werift": "^0.24.1"