vibedate 0.8.3 → 0.8.5

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.
@@ -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,10 +243,15 @@ 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() {
@@ -162,7 +273,7 @@ var FileConsentStore = class {
162
273
  }
163
274
  save(grants) {
164
275
  mkdirSync(path.dirname(this.file), { recursive: true });
165
- writeFileSync(this.file, JSON.stringify({ grants }, null, 2) + "\n", "utf8");
276
+ writeJsonAtomic(this.file, { grants });
166
277
  }
167
278
  };
168
279
  function createLedger(dir = defaultStateDir()) {
@@ -184,7 +295,7 @@ function connectProfile(snapshot, handle, dir = defaultStateDir()) {
184
295
  connectedAt: (/* @__PURE__ */ new Date()).toISOString()
185
296
  };
186
297
  mkdirSync(dir, { recursive: true });
187
- writeFileSync(profilePath(dir), JSON.stringify(state, null, 2) + "\n", "utf8");
298
+ writeJsonAtomic(profilePath(dir), state);
188
299
  return state;
189
300
  }
190
301
  function loadProfile(dir = defaultStateDir()) {
@@ -245,14 +356,10 @@ function saveHandle(handle, dir = defaultStateDir()) {
245
356
  );
246
357
  }
247
358
  mkdirSync(dir, { recursive: true });
248
- writeFileSync(handleFilePath(dir), JSON.stringify({ handle: canonical }, null, 2) + "\n", "utf8");
359
+ writeJsonAtomic(handleFilePath(dir), { handle: canonical });
249
360
  const existing = loadProfile(dir);
250
361
  if (existing !== null) {
251
- writeFileSync(
252
- profilePath(dir),
253
- JSON.stringify({ ...existing, handle: canonical }, null, 2) + "\n",
254
- "utf8"
255
- );
362
+ writeJsonAtomic(profilePath(dir), { ...existing, handle: canonical });
256
363
  }
257
364
  return canonical;
258
365
  }
@@ -270,7 +377,7 @@ function blocklistPath(dir) {
270
377
  }
271
378
  function persistBlocklist(blocked, dir) {
272
379
  mkdirSync(dir, { recursive: true });
273
- writeFileSync(blocklistPath(dir), JSON.stringify({ blocked }, null, 2) + "\n", "utf8");
380
+ writeJsonAtomic(blocklistPath(dir), { blocked });
274
381
  }
275
382
  function loadBlocklist(dir = defaultStateDir()) {
276
383
  try {
@@ -442,7 +549,7 @@ import { readFile } from "fs/promises";
442
549
  import { writeFileSync as writeFileSync3 } from "fs";
443
550
  import os2 from "os";
444
551
  import path3 from "path";
445
- var DEFAULT_CHUNK_BYTES = Math.floor(MAX_B64_CHUNK_LEN * 3 / 4);
552
+ var DEFAULT_CHUNK_BYTES = MAX_BINARY_CHUNK_BYTES;
446
553
  var MIME_BY_EXT = {
447
554
  ".png": "image/png",
448
555
  ".jpg": "image/jpeg",
@@ -455,11 +562,32 @@ var MIME_BY_EXT = {
455
562
  function inferMime(name) {
456
563
  return MIME_BY_EXT[path3.extname(name).toLowerCase()] ?? "application/octet-stream";
457
564
  }
458
- function writeFrame(socket, line) {
459
- return new Promise((resolve) => {
460
- const ok = socket.write(line);
565
+ function writeBytes(socket, data) {
566
+ return new Promise((resolve, reject) => {
567
+ const ok = socket.write(data);
461
568
  if (ok) resolve();
462
- else socket.once("drain", () => resolve());
569
+ else {
570
+ const onDrain = () => {
571
+ cleanup();
572
+ resolve();
573
+ };
574
+ const onClose = () => {
575
+ cleanup();
576
+ reject(new Error("Socket closed before drain"));
577
+ };
578
+ const onError = (err) => {
579
+ cleanup();
580
+ reject(err);
581
+ };
582
+ const cleanup = () => {
583
+ socket.removeListener("drain", onDrain);
584
+ socket.removeListener("close", onClose);
585
+ socket.removeListener("error", onError);
586
+ };
587
+ socket.once("drain", onDrain);
588
+ socket.once("close", onClose);
589
+ socket.once("error", onError);
590
+ }
463
591
  });
464
592
  }
465
593
  async function sendMedia(opts) {
@@ -471,15 +599,23 @@ async function sendMedia(opts) {
471
599
  }
472
600
  if (mime.length > MAX_MIME_LEN) throw new Error(`mime too long: ${mime.length} > ${MAX_MIME_LEN}`);
473
601
  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;
602
+ await writeBytes(socket, serializeFrame({ t: "media-start", id, mime, size, name }) + "\n");
603
+ const chunkBytes = Math.min(
604
+ opts.chunkBytes ?? DEFAULT_CHUNK_BYTES,
605
+ opts.legacyJson ? Math.floor(16 * 1024 * 3 / 4) : MAX_BINARY_CHUNK_BYTES
606
+ );
476
607
  let seq = 0;
477
608
  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");
609
+ const slice = data.subarray(off, off + chunkBytes);
610
+ if (opts.legacyJson) {
611
+ const b64 = slice.toString("base64");
612
+ await writeBytes(socket, serializeFrame({ t: "media-chunk", id, seq, b64 }) + "\n");
613
+ } else {
614
+ await writeBytes(socket, serializeBinaryMediaChunk({ id, seq, data: slice }));
615
+ }
480
616
  seq++;
481
617
  }
482
- await writeFrame(socket, serializeFrame({ t: "media-end", id }) + "\n");
618
+ await writeBytes(socket, serializeFrame({ t: "media-end", id }) + "\n");
483
619
  return { id, size };
484
620
  }
485
621
  async function sendMediaFile(opts) {
@@ -492,7 +628,8 @@ async function sendMediaFile(opts) {
492
628
  mime,
493
629
  name,
494
630
  id: opts.id,
495
- chunkBytes: opts.chunkBytes
631
+ chunkBytes: opts.chunkBytes,
632
+ legacyJson: opts.legacyJson
496
633
  });
497
634
  }
498
635
  function safeName(id, name) {
@@ -533,9 +670,20 @@ var MediaReceiver = class {
533
670
  return;
534
671
  }
535
672
  let bytes;
536
- try {
537
- bytes = Buffer.from(frame.b64, "base64");
538
- } catch {
673
+ if ("data" in frame && Buffer.isBuffer(frame.data)) {
674
+ bytes = frame.data;
675
+ if (bytes.length === 0 || bytes.length > MAX_BINARY_CHUNK_BYTES) {
676
+ this.abort(frame.id);
677
+ return;
678
+ }
679
+ } else if ("b64" in frame && typeof frame.b64 === "string") {
680
+ try {
681
+ bytes = Buffer.from(frame.b64, "base64");
682
+ } catch {
683
+ this.abort(frame.id);
684
+ return;
685
+ }
686
+ } else {
539
687
  this.abort(frame.id);
540
688
  return;
541
689
  }
@@ -558,7 +706,9 @@ var MediaReceiver = class {
558
706
  const filePath = path3.join(this.opts.tmpDir ?? os2.tmpdir(), safeName(frame.id, tx.name));
559
707
  try {
560
708
  writeFileSync3(filePath, buf);
561
- } catch {
709
+ } catch (err) {
710
+ const error = err instanceof Error ? err : new Error(String(err));
711
+ this.onMedia({ mime: tx.mime, name: tx.name, path: filePath, size: tx.received, error });
562
712
  return;
563
713
  }
564
714
  this.onMedia({ mime: tx.mime, name: tx.name, path: filePath, size: tx.received });
@@ -576,7 +726,7 @@ function createPeerLink(socket, hello, initialBuffer = "", linkOpts = {}) {
576
726
  const mediaCbs = /* @__PURE__ */ new Set();
577
727
  const signalCbs = /* @__PURE__ */ new Set();
578
728
  const closeCbs = /* @__PURE__ */ new Set();
579
- let buf = initialBuffer;
729
+ let buf = typeof initialBuffer === "string" ? Buffer.from(initialBuffer, "utf8") : Buffer.from(initialBuffer);
580
730
  let closed = false;
581
731
  let mediaReceiver;
582
732
  const ensureMediaReceiver = () => {
@@ -624,19 +774,15 @@ function createPeerLink(socket, hello, initialBuffer = "", linkOpts = {}) {
624
774
  }
625
775
  };
626
776
  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
- }
777
+ if (buf.length === 0) return;
778
+ const { frames, rest } = pullFramesFromBuffer(buf);
779
+ buf = rest;
780
+ for (const frame of frames) dispatch(frame);
636
781
  };
637
782
  pump();
638
783
  socket.on("data", (chunk) => {
639
- buf += chunk.toString("utf8");
784
+ const bytes = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
785
+ buf = buf.length === 0 ? Buffer.from(bytes) : Buffer.concat([buf, bytes]);
640
786
  pump();
641
787
  });
642
788
  socket.on("end", () => {
@@ -659,6 +805,9 @@ function createPeerLink(socket, hello, initialBuffer = "", linkOpts = {}) {
659
805
  });
660
806
  return {
661
807
  hello,
808
+ get closed() {
809
+ return closed;
810
+ },
662
811
  send(text) {
663
812
  if (closed) return;
664
813
  const frame = { t: "msg", id: randomUUID2(), text, at: Date.now() };
@@ -863,15 +1012,18 @@ async function startDiscovery(opts) {
863
1012
  ...hello.sig !== void 0 ? { sig: hello.sig } : {}
864
1013
  }) + "\n"
865
1014
  );
866
- let buf = "";
1015
+ let buf = Buffer.alloc(0);
867
1016
  let handedOff = false;
868
1017
  const onData = (chunk) => {
869
1018
  if (handedOff) return;
870
- buf += chunk.toString("utf8");
1019
+ buf = buf.length === 0 ? Buffer.from(chunk) : Buffer.concat([buf, chunk]);
871
1020
  let nl;
872
- while ((nl = buf.indexOf("\n")) >= 0) {
873
- const line = buf.slice(0, nl);
874
- buf = buf.slice(nl + 1);
1021
+ while ((nl = buf.indexOf(
1022
+ 10
1023
+ /* \n */
1024
+ )) >= 0) {
1025
+ const line = buf.subarray(0, nl).toString("utf8");
1026
+ buf = buf.subarray(nl + 1);
875
1027
  if (line.trim() === "") continue;
876
1028
  const frame = parseFrame(line);
877
1029
  if (frame === null) continue;
@@ -917,7 +1069,7 @@ async function startDiscovery(opts) {
917
1069
  });
918
1070
  onLink(link);
919
1071
  }
920
- buf = "";
1072
+ buf = Buffer.alloc(0);
921
1073
  return;
922
1074
  }
923
1075
  };
@@ -1067,6 +1219,14 @@ async function createNostrRelayLink(opts) {
1067
1219
  return;
1068
1220
  }
1069
1221
  peerNostrPubkey = event.pubkey;
1222
+ try {
1223
+ const parsed = JSON.parse(plaintext);
1224
+ if (parsed && typeof parsed === "object" && "__vibe_rtc" in parsed) {
1225
+ for (const cb of signalCbs) cb(parsed.__vibe_rtc);
1226
+ return;
1227
+ }
1228
+ } catch {
1229
+ }
1070
1230
  const msg = { id: event.id, text: plaintext, at: event.created_at * 1e3 };
1071
1231
  for (const cb of messageCbs) cb(msg);
1072
1232
  return;
@@ -1075,6 +1235,9 @@ async function createNostrRelayLink(opts) {
1075
1235
  publishPresence();
1076
1236
  return {
1077
1237
  hello,
1238
+ get closed() {
1239
+ return closed;
1240
+ },
1078
1241
  send(text) {
1079
1242
  if (closed) return;
1080
1243
  if (peerNostrPubkey === void 0) {
@@ -1088,9 +1251,14 @@ async function createNostrRelayLink(opts) {
1088
1251
  async sendMedia() {
1089
1252
  return { id: "", size: 0 };
1090
1253
  },
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() {
1254
+ sendSignal(frame) {
1255
+ if (closed) return;
1256
+ const payload = JSON.stringify({ __vibe_rtc: frame });
1257
+ if (peerNostrPubkey === void 0) {
1258
+ pending.push(payload);
1259
+ return;
1260
+ }
1261
+ sendEncrypted(payload, peerNostrPubkey);
1094
1262
  },
1095
1263
  onMessage(cb) {
1096
1264
  messageCbs.add(cb);
@@ -1163,6 +1331,7 @@ var ROOM_TOPIC_PREFIX = "vibedate-room:";
1163
1331
  function roomTopic(name) {
1164
1332
  return createHash3("sha256").update(`${ROOM_TOPIC_PREFIX}${name}`, "utf8").digest();
1165
1333
  }
1334
+ var MAX_ROOM = 32;
1166
1335
  async function startRoom(opts) {
1167
1336
  const topic = roomTopic(opts.room);
1168
1337
  const entries = /* @__PURE__ */ new Map();
@@ -1186,7 +1355,19 @@ async function startRoom(opts) {
1186
1355
  ...opts.stateDir === void 0 ? {} : { stateDir: opts.stateDir },
1187
1356
  ...opts.notify === void 0 ? {} : { notify: opts.notify },
1188
1357
  onLink: (link) => {
1358
+ const isSelf = link.hello.pubkey !== void 0 && opts.hello.pubkey !== void 0 ? link.hello.pubkey === opts.hello.pubkey : link.hello.handle === opts.hello.handle;
1359
+ if (isSelf) {
1360
+ link.close();
1361
+ return;
1362
+ }
1189
1363
  const handle = link.hello.handle;
1364
+ if (!entries.has(handle) && entries.size >= MAX_ROOM) {
1365
+ link.send(`Room is full (${MAX_ROOM} members max). Try again later.`);
1366
+ link.close();
1367
+ return;
1368
+ }
1369
+ const cur = entries.get(handle);
1370
+ if (cur) cur.link.close();
1190
1371
  entries.set(handle, { hello: link.hello, link });
1191
1372
  memberHellos.set(handle, link.hello);
1192
1373
  fireRoster();
@@ -1197,8 +1378,8 @@ async function startRoom(opts) {
1197
1378
  for (const cb of signalCbs) cb(handle, frame);
1198
1379
  });
1199
1380
  link.onClose(() => {
1200
- const cur = entries.get(handle);
1201
- if (cur && cur.link === link) {
1381
+ const cur2 = entries.get(handle);
1382
+ if (cur2 && cur2.link === link) {
1202
1383
  entries.delete(handle);
1203
1384
  memberHellos.delete(handle);
1204
1385
  fireRoster();
@@ -29,7 +29,7 @@ import {
29
29
  signHelloClaims,
30
30
  startDiscovery,
31
31
  startRoom
32
- } from "./chunk-3FJDVN72.js";
32
+ } from "./chunk-KIFZHCWP.js";
33
33
 
34
34
  // src/mcp.ts
35
35
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -14890,6 +14890,7 @@ function uninstallDaemonService(opts) {
14890
14890
 
14891
14891
  // src/pairing.ts
14892
14892
  var MAX_BUFFERED = 100;
14893
+ var MAX_QUEUE = 100;
14893
14894
  function createPairing() {
14894
14895
  const queue = [];
14895
14896
  const matchCbs = /* @__PURE__ */ new Set();
@@ -14923,7 +14924,14 @@ function createPairing() {
14923
14924
  link.onClose(() => {
14924
14925
  buffers.delete(link);
14925
14926
  if (current === link) {
14926
- const nextUp = queue.shift();
14927
+ let nextUp;
14928
+ while (queue.length > 0) {
14929
+ const cand = queue.shift();
14930
+ if (!cand.closed) {
14931
+ nextUp = cand;
14932
+ break;
14933
+ }
14934
+ }
14927
14935
  setCurrent(nextUp);
14928
14936
  } else {
14929
14937
  const idx = queue.indexOf(link);
@@ -14980,6 +14988,10 @@ function createPairing() {
14980
14988
  setCurrent(link);
14981
14989
  } else {
14982
14990
  queue.push(link);
14991
+ if (queue.length > MAX_QUEUE) {
14992
+ const oldest = queue.shift();
14993
+ oldest.close();
14994
+ }
14983
14995
  }
14984
14996
  },
14985
14997
  next() {
@@ -14987,7 +14999,14 @@ function createPairing() {
14987
14999
  current.close();
14988
15000
  current = void 0;
14989
15001
  }
14990
- const nextUp = queue.shift();
15002
+ let nextUp;
15003
+ while (queue.length > 0) {
15004
+ const cand = queue.shift();
15005
+ if (!cand.closed) {
15006
+ nextUp = cand;
15007
+ break;
15008
+ }
15009
+ }
14991
15010
  setCurrent(nextUp);
14992
15011
  return current;
14993
15012
  },
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-VHO7JWSS.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-KIFZHCWP.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.5";
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 { P as PeerHello, a as PeerLink } from './room-B-0DreC0.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-B-0DreC0.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-KIFZHCWP.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-DpZhNvuu.js';
2
+ import { a as PeerLink, P as PeerHello, f as RoomSession, b as DiscoverySession, d as RoomMessage } from './room-B-0DreC0.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-XUWNLMOY.js";
7
- import "./chunk-3FJDVN72.js";
6
+ } from "./chunk-VHO7JWSS.js";
7
+ import "./chunk-KIFZHCWP.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. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibedate",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
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",
@@ -25,7 +25,8 @@
25
25
  "build": "tsup src/index.ts src/cli.ts src/mcp.ts --format esm --dts --clean",
26
26
  "typecheck": "tsc --noEmit",
27
27
  "test": "vitest run",
28
- "test:watch": "vitest"
28
+ "test:watch": "vitest",
29
+ "bench": "tsx bench/run.ts"
29
30
  },
30
31
  "dependencies": {
31
32
  "@modelcontextprotocol/sdk": "^1.0.0",
@@ -37,6 +38,7 @@
37
38
  "@types/node": "^20.11.0",
38
39
  "hyperdht": "^6.33.0",
39
40
  "tsup": "^8.0.0",
41
+ "tsx": "^4.23.1",
40
42
  "typescript": "^5.4.0",
41
43
  "vitest": "^1.6.0",
42
44
  "werift": "^0.24.1"