vibedate 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.d.ts CHANGED
@@ -44,5 +44,10 @@ declare function formatAgo(iso: string, now?: Date): string;
44
44
  * Interactive TTY behavior is unchanged: Ctrl+D / EOF still exits.
45
45
  */
46
46
  declare function shouldKeepAlive(flag: boolean, stdinIsTTY: boolean | undefined): boolean;
47
+ declare function parseSendCommand(text: string): {
48
+ path: string;
49
+ } | {
50
+ error: 'missing_path';
51
+ } | null;
47
52
 
48
- export { type Command, type ParsedArgs, formatAgo, parseArgs, shouldKeepAlive };
53
+ export { type Command, type ParsedArgs, formatAgo, parseArgs, parseSendCommand, shouldKeepAlive };
package/dist/cli.js CHANGED
@@ -56,6 +56,9 @@ import process2 from "process";
56
56
  // src/server.ts
57
57
  import http from "http";
58
58
  import { randomUUID } from "crypto";
59
+ import fs from "fs";
60
+ import os from "os";
61
+ import path from "path";
59
62
  import { makeEvent, notify as vibeCoreNotify } from "@pooriaarab/vibe-core";
60
63
 
61
64
  // src/web-app-html.ts
@@ -599,15 +602,23 @@ var webAppHtml = `<!DOCTYPE html>
599
602
  max-width: 85%; padding: 7px 11px; border-radius: 13px;
600
603
  font-size: .82rem; line-height: 1.4; word-break: break-word; white-space: pre-wrap;
601
604
  }
605
+ .cp-msg img, .cp-msg video { max-width: 100%; border-radius: 8px; margin-top: 4px; display: block; }
606
+ .cp-msg a.media-link { color: var(--coral); text-decoration: underline; word-break: break-all; }
602
607
  .cp-msg.them{ align-self: flex-start; background: rgba(248,239,232,.07); border: 1px solid var(--border); }
603
608
  .cp-msg.you{ align-self: flex-end; background: rgba(255,122,104,.16); border: 1px solid rgba(255,122,104,.32); }
604
609
  .cp-msg.sys{ align-self: center; background: transparent; border: 0; color: var(--muted-2); font-size: .72rem; padding: 2px 6px; }
605
610
  .chat-panel .cp-inputrow{ display: flex; gap: 8px; padding: 10px; border-top: 1px solid var(--border); }
606
- .chat-panel .cp-inputrow input{
611
+ .chat-panel .cp-inputrow input[type="text"]{
607
612
  flex: 1; border: 1px solid var(--border-2); border-radius: 10px; background: rgba(0,0,0,.22);
608
613
  color: var(--fg); padding: 9px 11px; font: inherit; font-size: .82rem; outline: none; min-width: 0;
609
614
  }
610
- .chat-panel .cp-inputrow input:focus{ border-color: var(--coral); }
615
+ .chat-panel .cp-inputrow input[type="text"]:focus{ border-color: var(--coral); }
616
+ .chat-panel .cp-attach{
617
+ border: 1px solid var(--border-2); border-radius: 10px; background: rgba(0,0,0,.22);
618
+ color: var(--muted); padding: 9px 12px; font-size: 1rem; flex-shrink: 0;
619
+ transition: color var(--dur-fast) ease, border-color var(--dur-fast) ease;
620
+ }
621
+ .chat-panel .cp-attach:hover{ color: var(--fg); border-color: var(--muted-2); }
611
622
  .chat-panel .cp-send{
612
623
  border: 0; border-radius: 10px; padding: 9px 14px; font-weight: 700; font-size: .8rem;
613
624
  background: linear-gradient(180deg, var(--coral), var(--coral-dim)); color: #2a1109;
@@ -786,6 +797,8 @@ var webAppHtml = `<!DOCTYPE html>
786
797
  </div>
787
798
  <div class="cp-msgs" id="chatMsgs"></div>
788
799
  <div class="cp-inputrow">
800
+ <button class="cp-attach" id="chatAttachBtn" type="button" aria-label="Attach file" title="Attach file">\u{1F4CE}</button>
801
+ <input type="file" id="chatFileInput" style="display:none;">
789
802
  <input id="chatInput" type="text" maxlength="4000" placeholder="message&hellip;" autocomplete="off">
790
803
  <button class="cp-send" id="chatSend" type="button">Send</button>
791
804
  </div>
@@ -1304,6 +1317,8 @@ var webAppHtml = `<!DOCTYPE html>
1304
1317
  var chatSub = document.getElementById("chatSub");
1305
1318
  var chatMsgs = document.getElementById("chatMsgs");
1306
1319
  var chatInput = document.getElementById("chatInput");
1320
+ var chatAttachBtn = document.getElementById("chatAttachBtn");
1321
+ var chatFileInput = document.getElementById("chatFileInput");
1307
1322
  var chatSend = document.getElementById("chatSend");
1308
1323
  var chatClose = document.getElementById("chatClose");
1309
1324
 
@@ -1311,6 +1326,7 @@ var webAppHtml = `<!DOCTYPE html>
1311
1326
  var conversations = {}; // handle -> [{from:"you"|"them"|"sys", text}]
1312
1327
  var unread = {}; // handle -> count of unseen incoming messages
1313
1328
  var chatLoops = {}; // handle -> true while its poll loop is running
1329
+ var mediaLoops = {}; // handle -> true while its poll loop is running
1314
1330
  var MAX_CHAT_KEPT = 200; // per-conversation local cap (mirrors the server)
1315
1331
 
1316
1332
  function fetchMessage(handle, timeoutMs){
@@ -1321,6 +1337,14 @@ var webAppHtml = `<!DOCTYPE html>
1321
1337
  .catch(function(e){ clearTimeout(t); throw e; });
1322
1338
  }
1323
1339
 
1340
+ function fetchMedia(handle, timeoutMs){
1341
+ var ctrl = new AbortController();
1342
+ var t = setTimeout(function(){ ctrl.abort(); }, timeoutMs);
1343
+ return fetch("/live/media?handle=" + encodeURIComponent(handle), { signal: ctrl.signal })
1344
+ .then(function(r){ clearTimeout(t); return r; })
1345
+ .catch(function(e){ clearTimeout(t); throw e; });
1346
+ }
1347
+
1324
1348
  function postChat(handle, text){
1325
1349
  return fetch("/live/message", {
1326
1350
  method: "POST",
@@ -1329,6 +1353,14 @@ var webAppHtml = `<!DOCTYPE html>
1329
1353
  });
1330
1354
  }
1331
1355
 
1356
+ function postMedia(handle, name, mime, dataB64){
1357
+ return fetch("/live/media", {
1358
+ method: "POST",
1359
+ headers: { "content-type": "application/json" },
1360
+ body: JSON.stringify({ handle: handle, name: name, mime: mime, dataB64: dataB64 })
1361
+ });
1362
+ }
1363
+
1332
1364
  function peerKnown(handle){
1333
1365
  for (var i = 0; i < knownPeers.length; i++){ if (knownPeers[i].handle === handle) return true; }
1334
1366
  return false;
@@ -1343,27 +1375,51 @@ var webAppHtml = `<!DOCTYPE html>
1343
1375
  // One long-poll loop per connected peer: started when the peer is first
1344
1376
  // seen, stopped when it vanishes (its mailbox on the server is gone).
1345
1377
  function ensureChatLoop(handle){
1346
- if (chatLoops[handle]) return;
1347
- chatLoops[handle] = true;
1348
- (async function(){
1349
- while (peerKnown(handle)) {
1350
- var res;
1351
- try { res = await fetchMessage(handle, 30000); }
1352
- catch(e){ await sleep(1000); continue; }
1353
- if (!res || res.status !== 200) { await sleep(1000); continue; }
1354
- var data = await res.json();
1355
- var m = data && data.message;
1356
- if (!m) continue; // timed out empty
1357
- pushChat(handle, { from: "them", text: m.text });
1358
- if (chatWith === handle) {
1359
- renderChat();
1360
- } else {
1361
- unread[handle] = (unread[handle] || 0) + 1;
1362
- renderLiveRows(knownPeers);
1378
+ if (!chatLoops[handle]) {
1379
+ chatLoops[handle] = true;
1380
+ (async function(){
1381
+ while (peerKnown(handle)) {
1382
+ var res;
1383
+ try { res = await fetchMessage(handle, 30000); }
1384
+ catch(e){ await sleep(1000); continue; }
1385
+ if (!res || res.status !== 200) { await sleep(1000); continue; }
1386
+ var data = await res.json();
1387
+ var m = data && data.message;
1388
+ if (!m) continue; // timed out empty
1389
+ pushChat(handle, { from: "them", text: m.text });
1390
+ if (chatWith === handle) {
1391
+ renderChat();
1392
+ } else {
1393
+ unread[handle] = (unread[handle] || 0) + 1;
1394
+ renderLiveRows(knownPeers);
1395
+ }
1363
1396
  }
1364
- }
1365
- delete chatLoops[handle];
1366
- })();
1397
+ delete chatLoops[handle];
1398
+ })();
1399
+ }
1400
+
1401
+ if (!mediaLoops[handle]) {
1402
+ mediaLoops[handle] = true;
1403
+ (async function(){
1404
+ while (peerKnown(handle)) {
1405
+ var res;
1406
+ try { res = await fetchMedia(handle, 30000); }
1407
+ catch(e){ await sleep(1000); continue; }
1408
+ if (!res || res.status !== 200) { await sleep(1000); continue; }
1409
+ var data = await res.json();
1410
+ var m = data && data.media;
1411
+ if (!m) continue; // timed out empty
1412
+ pushChat(handle, { from: "them", media: m });
1413
+ if (chatWith === handle) {
1414
+ renderChat();
1415
+ } else {
1416
+ unread[handle] = (unread[handle] || 0) + 1;
1417
+ renderLiveRows(knownPeers);
1418
+ }
1419
+ }
1420
+ delete mediaLoops[handle];
1421
+ })();
1422
+ }
1367
1423
  }
1368
1424
 
1369
1425
  function renderChat(){
@@ -1379,8 +1435,30 @@ var webAppHtml = `<!DOCTYPE html>
1379
1435
  conv.forEach(function(m){
1380
1436
  var el = document.createElement("div");
1381
1437
  el.className = "cp-msg " + (m.from === "you" ? "you" : m.from === "sys" ? "sys" : "them");
1382
- // textContent only \u2014 a peer's text is never parsed as HTML.
1383
- el.textContent = m.text;
1438
+ if (m.media) {
1439
+ var isImage = m.media.mime.indexOf("image/") === 0;
1440
+ var isVideo = m.media.mime.indexOf("video/") === 0;
1441
+ if (isImage) {
1442
+ var img = document.createElement("img");
1443
+ img.src = "data:" + m.media.mime + ";base64," + m.media.dataB64;
1444
+ el.appendChild(img);
1445
+ } else if (isVideo) {
1446
+ var vid = document.createElement("video");
1447
+ vid.src = "data:" + m.media.mime + ";base64," + m.media.dataB64;
1448
+ vid.controls = true;
1449
+ el.appendChild(vid);
1450
+ } else {
1451
+ var a = document.createElement("a");
1452
+ a.className = "media-link";
1453
+ a.href = "data:" + (m.media.mime || "application/octet-stream") + ";base64," + m.media.dataB64;
1454
+ a.download = m.media.name;
1455
+ a.textContent = "\u{1F4CE} " + m.media.name;
1456
+ el.appendChild(a);
1457
+ }
1458
+ } else {
1459
+ // textContent only \u2014 a peer's text is never parsed as HTML.
1460
+ el.textContent = m.text;
1461
+ }
1384
1462
  chatMsgs.appendChild(el);
1385
1463
  });
1386
1464
  chatMsgs.scrollTop = chatMsgs.scrollHeight;
@@ -1422,6 +1500,34 @@ var webAppHtml = `<!DOCTYPE html>
1422
1500
  chatSend.addEventListener("click", sendChat);
1423
1501
  chatInput.addEventListener("keydown", function(e){ if (e.key === "Enter") sendChat(); });
1424
1502
 
1503
+ chatAttachBtn.addEventListener("click", function(){
1504
+ if (!chatWith) return;
1505
+ chatFileInput.click();
1506
+ });
1507
+ chatFileInput.addEventListener("change", function(e){
1508
+ var target = chatWith;
1509
+ if (!target) return;
1510
+ var file = e.target.files[0];
1511
+ if (!file) return;
1512
+ chatFileInput.value = ""; // clear
1513
+ var reader = new FileReader();
1514
+ reader.onload = function(evt) {
1515
+ var res = evt.target.result; // data:mime;base64,.....
1516
+ var mime = res.split(';')[0].split(':')[1] || '';
1517
+ var b64 = res.split(',')[1];
1518
+ postMedia(target, file.name, mime, b64).then(function(r){
1519
+ pushChat(target, r && r.status === 200
1520
+ ? { from: "you", media: { name: file.name, mime: mime, dataB64: b64 } }
1521
+ : { from: "sys", text: "(file not delivered \u2014 too large or peer offline?)" });
1522
+ if (chatWith === target) renderChat();
1523
+ }).catch(function(){
1524
+ pushChat(target, { from: "sys", text: "(file not delivered \u2014 server unreachable)" });
1525
+ if (chatWith === target) renderChat();
1526
+ });
1527
+ };
1528
+ reader.readAsDataURL(file);
1529
+ });
1530
+
1425
1531
  // Render the live-peers rows: one per connected peer with its verification
1426
1532
  // marks, a Chat button (opens the conversation), and a Call button that
1427
1533
  // rings THAT peer specifically.
@@ -1750,7 +1856,7 @@ function createLiveBridge() {
1750
1856
  },
1751
1857
  addLink(link) {
1752
1858
  const handle2 = link.hello.handle;
1753
- boxes.set(handle2, { link, incoming: [], messages: [] });
1859
+ boxes.set(handle2, { link, incoming: [], messages: [], media: [] });
1754
1860
  link.onSignal((f) => {
1755
1861
  const mb = boxes.get(handle2);
1756
1862
  if (mb) mb.incoming.push(f);
@@ -1763,6 +1869,20 @@ function createLiveBridge() {
1763
1869
  mb.messages.splice(0, mb.messages.length - MAX_QUEUED_MESSAGES);
1764
1870
  }
1765
1871
  });
1872
+ link.onMedia((m) => {
1873
+ const mb = boxes.get(handle2);
1874
+ if (!mb) {
1875
+ fs.unlink(m.path, () => {
1876
+ });
1877
+ return;
1878
+ }
1879
+ mb.media.push({ ...m, name: sanitizePeerText(m.name), mime: sanitizePeerText(m.mime) });
1880
+ if (mb.media.length > MAX_QUEUED_MESSAGES) {
1881
+ const dropped = mb.media.splice(0, mb.media.length - MAX_QUEUED_MESSAGES);
1882
+ for (const d of dropped) fs.unlink(d.path, () => {
1883
+ });
1884
+ }
1885
+ });
1766
1886
  link.onClose(() => {
1767
1887
  const cur = boxes.get(handle2);
1768
1888
  if (cur && cur.link === link) boxes.delete(handle2);
@@ -1799,6 +1919,39 @@ function createLiveBridge() {
1799
1919
  if (cur.messages.length > 0) return cur.messages.shift() ?? null;
1800
1920
  }
1801
1921
  return null;
1922
+ },
1923
+ async sendMedia(handle2, filePath) {
1924
+ const mb = boxes.get(handle2);
1925
+ if (!mb) throw new Error("Peer not found");
1926
+ await mb.link.sendMedia(filePath);
1927
+ },
1928
+ async pollMedia(handle2, timeoutMs) {
1929
+ const mb = boxes.get(handle2);
1930
+ if (!mb) return null;
1931
+ let targetMedia = null;
1932
+ if (mb.media.length > 0) {
1933
+ targetMedia = mb.media.shift() ?? null;
1934
+ } else {
1935
+ const deadline = Date.now() + timeoutMs;
1936
+ while (Date.now() < deadline) {
1937
+ await new Promise((r) => setTimeout(r, 100));
1938
+ const cur = boxes.get(handle2);
1939
+ if (!cur) return null;
1940
+ if (cur.media.length > 0) {
1941
+ targetMedia = cur.media.shift() ?? null;
1942
+ break;
1943
+ }
1944
+ }
1945
+ }
1946
+ if (!targetMedia) return null;
1947
+ try {
1948
+ const buf = await fs.promises.readFile(targetMedia.path);
1949
+ await fs.promises.unlink(targetMedia.path).catch(() => {
1950
+ });
1951
+ return { name: targetMedia.name, mime: targetMedia.mime, dataB64: buf.toString("base64") };
1952
+ } catch (err) {
1953
+ return null;
1954
+ }
1802
1955
  }
1803
1956
  };
1804
1957
  return bridge;
@@ -1916,10 +2069,18 @@ function send(res, status, contentType, body) {
1916
2069
  function sendJson(res, status, data) {
1917
2070
  send(res, status, "application/json; charset=utf-8", JSON.stringify(data));
1918
2071
  }
1919
- function readBody(req) {
2072
+ function readBody(req, maxLength) {
1920
2073
  return new Promise((resolve, reject) => {
1921
2074
  const chunks = [];
1922
- req.on("data", (c) => chunks.push(c));
2075
+ let len = 0;
2076
+ req.on("data", (c) => {
2077
+ chunks.push(c);
2078
+ len += c.length;
2079
+ if (maxLength !== void 0 && len > maxLength) {
2080
+ req.destroy();
2081
+ reject(new Error("payload too large"));
2082
+ }
2083
+ });
1923
2084
  req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
1924
2085
  req.on("error", reject);
1925
2086
  });
@@ -2110,6 +2271,69 @@ async function handle(req, res, opts) {
2110
2271
  sendJson(res, 200, { ok: true });
2111
2272
  return;
2112
2273
  }
2274
+ if (req.method === "GET" && pathname === "/live/media") {
2275
+ const live = opts.live;
2276
+ if (!live) {
2277
+ sendJson(res, 200, { media: null, reason: "live-not-attached" });
2278
+ return;
2279
+ }
2280
+ const handle2 = url.searchParams.get("handle") ?? "";
2281
+ if (handle2 === "") {
2282
+ sendJson(res, 400, { error: "missing handle" });
2283
+ return;
2284
+ }
2285
+ const media = await live.pollMedia(handle2, 25e3);
2286
+ if (req.destroyed || res.writableEnded) return;
2287
+ sendJson(res, 200, { media });
2288
+ return;
2289
+ }
2290
+ if (req.method === "POST" && pathname === "/live/media") {
2291
+ const live = opts.live;
2292
+ if (!live) {
2293
+ sendJson(res, 400, { error: "live-not-attached" });
2294
+ return;
2295
+ }
2296
+ let body;
2297
+ try {
2298
+ body = await readBody(req, 35 * 1024 * 1024);
2299
+ } catch (e) {
2300
+ sendJson(res, 413, { error: "payload too large" });
2301
+ return;
2302
+ }
2303
+ let parsed = {};
2304
+ try {
2305
+ parsed = JSON.parse(body);
2306
+ } catch {
2307
+ sendJson(res, 400, { error: "invalid JSON body" });
2308
+ return;
2309
+ }
2310
+ const handle2 = typeof parsed["handle"] === "string" ? parsed["handle"] : "";
2311
+ const name = typeof parsed["name"] === "string" ? parsed["name"] : "";
2312
+ const mime = typeof parsed["mime"] === "string" ? parsed["mime"] : "";
2313
+ const dataB64 = typeof parsed["dataB64"] === "string" ? parsed["dataB64"] : "";
2314
+ if (handle2 === "" || name === "" || mime === "" || dataB64 === "") {
2315
+ sendJson(res, 400, { error: "missing handle, name, mime, or dataB64" });
2316
+ return;
2317
+ }
2318
+ const decoded = Buffer.from(dataB64, "base64");
2319
+ if (decoded.length > 25 * 1024 * 1024) {
2320
+ sendJson(res, 413, { error: "file exceeds 25MiB limit" });
2321
+ return;
2322
+ }
2323
+ const tmpPath = path.join(os.tmpdir(), `vibe-media-send-${randomUUID()}`);
2324
+ await fs.promises.writeFile(tmpPath, decoded);
2325
+ try {
2326
+ await live.sendMedia(handle2, tmpPath);
2327
+ } catch (err) {
2328
+ sendJson(res, 500, { error: err.message });
2329
+ return;
2330
+ } finally {
2331
+ await fs.promises.unlink(tmpPath).catch(() => {
2332
+ });
2333
+ }
2334
+ sendJson(res, 200, { ok: true });
2335
+ return;
2336
+ }
2113
2337
  if (req.method === "GET" && pathname === "/api/room") {
2114
2338
  const room = opts.room;
2115
2339
  sendJson(res, 200, room ? { room: room.name, self: room.self ?? null, members: room.members } : { room: null, self: null, members: [] });
@@ -2204,7 +2428,7 @@ async function handle(req, res, opts) {
2204
2428
  }
2205
2429
 
2206
2430
  // src/cli.ts
2207
- var VERSION = "0.8.0";
2431
+ var VERSION = "0.8.1";
2208
2432
  function parsePort(raw) {
2209
2433
  const n = Number(raw);
2210
2434
  if (!Number.isInteger(n) || n < 1 || n > 65535) return void 0;
@@ -2630,6 +2854,16 @@ async function cmdOpen(port, any, room) {
2630
2854
  function shouldKeepAlive(flag, stdinIsTTY) {
2631
2855
  return flag || stdinIsTTY !== true;
2632
2856
  }
2857
+ function parseSendCommand(text) {
2858
+ const parts = text.split(" ");
2859
+ const cmd = parts[0];
2860
+ if (cmd === "/send" || cmd === "/file" || cmd === "/image") {
2861
+ const path2 = text.slice(cmd.length).trim();
2862
+ if (!path2) return { error: "missing_path" };
2863
+ return { path: path2 };
2864
+ }
2865
+ return null;
2866
+ }
2633
2867
  async function cmdLiveViaRelay(profile, to) {
2634
2868
  const target = to !== void 0 ? normalizeHandle(to) : null;
2635
2869
  if (to !== void 0 && target === null) {
@@ -2799,6 +3033,12 @@ async function cmdLive(dating, any, to, keepAlive, viaRelay) {
2799
3033
  process2.stdout.write(` \u2605 found ${sanitizePeerText(link.hello.handle)} \u2014 auto-opening
2800
3034
  `);
2801
3035
  }
3036
+ link.onMedia((m) => {
3037
+ process2.stdout.write(
3038
+ ` \u{1F4CE} <${sanitizePeerText(link.hello.handle)}> sent ${sanitizePeerText(m.name)} \u2014 saved to ${m.path}
3039
+ `
3040
+ );
3041
+ });
2802
3042
  pairing.add(link);
2803
3043
  }
2804
3044
  });
@@ -2806,7 +3046,7 @@ async function cmdLive(dating, any, to, keepAlive, viaRelay) {
2806
3046
  ` topic: ${TOPIC_PREFIX}${profile.league} \u2192 ${session.topic.toString("hex").slice(0, 12)}\u2026${topics.length > 1 ? ` (+${topics.length - 1} more)` : ""}
2807
3047
  `
2808
3048
  );
2809
- process2.stdout.write(" type to chat \xB7 /next \xB7 /open <handle> \xB7 /quit\n");
3049
+ process2.stdout.write(" type to chat \xB7 /send <path> \xB7 /next \xB7 /open <handle> \xB7 /quit\n");
2810
3050
  process2.stdout.write(" video chat: live A/V runs in the web app \u2014 run `vibedating open`\n\n");
2811
3051
  const rl = readline.createInterface({ input: process2.stdin, terminal: false });
2812
3052
  const stop = () => {
@@ -2829,6 +3069,28 @@ async function cmdLive(dating, any, to, keepAlive, viaRelay) {
2829
3069
  const handle2 = text.slice("/open ".length).trim();
2830
3070
  if (pairing.open(handle2) === void 0) {
2831
3071
  process2.stdout.write(` \xB7 no available peer "${handle2}"
3072
+ `);
3073
+ }
3074
+ continue;
3075
+ }
3076
+ const sendRes = parseSendCommand(text);
3077
+ if (sendRes !== null) {
3078
+ if ("error" in sendRes) {
3079
+ process2.stdout.write(" usage: /send <path>\n");
3080
+ continue;
3081
+ }
3082
+ const cur3 = pairing.current();
3083
+ if (cur3 === void 0) {
3084
+ process2.stdout.write(" \xB7 no peer yet \u2014 waiting for a match\u2026\n");
3085
+ continue;
3086
+ }
3087
+ try {
3088
+ process2.stdout.write(` \u{1F4CE} sending ${sendRes.path}\u2026
3089
+ `);
3090
+ await cur3.sendMedia(sendRes.path);
3091
+ process2.stdout.write(" \u2713 sent\n");
3092
+ } catch (err) {
3093
+ process2.stdout.write(` \u2717 failed to send media: ${err instanceof Error ? err.message : String(err)}
2832
3094
  `);
2833
3095
  }
2834
3096
  continue;
@@ -3303,5 +3565,6 @@ if (entryUrl !== void 0) {
3303
3565
  export {
3304
3566
  formatAgo,
3305
3567
  parseArgs,
3568
+ parseSendCommand,
3306
3569
  shouldKeepAlive
3307
3570
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibedate",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Dating by tokens — matched by usage league across agentic CLIs. Raw usage stays local; only your league is shared. CLI + local web app + MCP. Local-first.",
5
5
  "type": "module",
6
6
  "license": "MIT",