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 +6 -1
- package/dist/cli.js +292 -29
- package/package.json +1 -1
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…" 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])
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1383
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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",
|