vibedate 0.7.2 → 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.js CHANGED
@@ -1,12 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- runMcp
4
- } from "./chunk-ZD6JBWEW.js";
3
+ createPairing,
4
+ daemonStatus,
5
+ ensureHandle,
6
+ installDaemonService,
7
+ removeDaemonState,
8
+ runMcp,
9
+ startDaemon,
10
+ stopDaemon,
11
+ uninstallDaemonService,
12
+ writeDaemonState
13
+ } from "./chunk-JBADPOR5.js";
5
14
  import {
6
15
  CANDIDATES,
7
- DEFAULT_HANDLE,
8
16
  LIVE_NOTICE,
9
- MAX_HANDLE_LEN,
10
17
  ROOM_TOPIC_PREFIX,
11
18
  TOPIC_PREFIX,
12
19
  addBlock,
@@ -23,7 +30,6 @@ import {
23
30
  leagueTopic,
24
31
  leaguesWithin,
25
32
  loadBlocklist,
26
- loadHandle,
27
33
  loadOrCreateIdentity,
28
34
  loadOrCreateNostrKey,
29
35
  loadPeers,
@@ -40,458 +46,19 @@ import {
40
46
  signHelloClaims,
41
47
  startDiscovery,
42
48
  startRoom
43
- } from "./chunk-HDHJLZZG.js";
49
+ } from "./chunk-S7MO5OSO.js";
44
50
 
45
51
  // src/cli.ts
46
52
  import readline from "readline";
47
53
  import "url";
48
54
  import process2 from "process";
49
55
 
50
- // src/handlegen.ts
51
- import { randomBytes } from "crypto";
52
- var FIRST = [
53
- "segfault",
54
- "yak",
55
- "vibe",
56
- "null",
57
- "async",
58
- "await",
59
- "heap",
60
- "stack",
61
- "sudo",
62
- "regex",
63
- "token",
64
- "prompt",
65
- "context",
66
- "merge",
67
- "rebase",
68
- "hotfix",
69
- "flaky",
70
- "cursed",
71
- "based",
72
- "quantum",
73
- "turbo",
74
- "neural",
75
- "agentic",
76
- "kernel",
77
- "docker",
78
- "kube",
79
- "lambda",
80
- "pointer",
81
- "buffer",
82
- "packet",
83
- "syscall",
84
- "runtime",
85
- "monad",
86
- "borrow",
87
- "cache",
88
- "deadlock",
89
- "localhost",
90
- "darkmode",
91
- "wasm",
92
- "diff"
93
- ];
94
- var SECOND = [
95
- "sommelier",
96
- "shaver",
97
- "goblin",
98
- "nomad",
99
- "gremlin",
100
- "wizard",
101
- "bard",
102
- "pirate",
103
- "ninja",
104
- "monk",
105
- "prophet",
106
- "oracle",
107
- "smith",
108
- "farmer",
109
- "whisperer",
110
- "tamer",
111
- "connoisseur",
112
- "maximalist",
113
- "enjoyer",
114
- "dealer",
115
- "herder",
116
- "wrangler",
117
- "juggler",
118
- "mechanic",
119
- "surgeon",
120
- "detective",
121
- "librarian",
122
- "alchemist",
123
- "overlord",
124
- "apprentice",
125
- "sensei",
126
- "chef",
127
- "dj",
128
- "ranger",
129
- "paladin",
130
- "barbarian",
131
- "summoner",
132
- "cartographer",
133
- "archivist",
134
- "plumber"
135
- ];
136
- var SUFFIX = [
137
- "prime",
138
- "9000",
139
- "3000",
140
- "max",
141
- "ultra",
142
- "xl",
143
- "mk2",
144
- "777",
145
- "404",
146
- "1337",
147
- "2077",
148
- "xd"
149
- ];
150
- function cryptoRand() {
151
- return randomBytes(4).readUInt32BE(0) / 2 ** 32;
152
- }
153
- function pick(list, rand) {
154
- return list[Math.min(list.length - 1, Math.floor(rand() * list.length))];
155
- }
156
- function generateHandle(rand = cryptoRand) {
157
- const first = pick(FIRST, rand);
158
- const second = pick(SECOND, rand);
159
- let body = `${first}_${second}`;
160
- if (rand() < 0.5) {
161
- const withSuffix = `${body}_${pick(SUFFIX, rand)}`;
162
- if (withSuffix.length + 1 <= MAX_HANDLE_LEN) body = withSuffix;
163
- }
164
- const canonical = normalizeHandle(body);
165
- if (canonical === null) throw new Error(`handle generator produced an invalid handle: ${body}`);
166
- return canonical;
167
- }
168
- function ensureHandle(dir = defaultStateDir()) {
169
- const env = process.env["VIBEDATING_HANDLE"];
170
- if (env !== void 0 && env.trim() !== "") {
171
- const canonical = normalizeHandle(env);
172
- if (canonical !== null) return { handle: canonical, generated: false };
173
- }
174
- const persisted = loadHandle(dir);
175
- if (persisted !== DEFAULT_HANDLE) return { handle: persisted, generated: false };
176
- const generated = generateHandle();
177
- return { handle: saveHandle(generated, dir), generated: true };
178
- }
179
-
180
- // src/daemon.ts
181
- import { spawn, spawnSync } from "child_process";
182
- import { mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from "fs";
183
- import os from "os";
184
- import path from "path";
185
- function daemonStatePath(dir) {
186
- return path.join(dir, "daemon.json");
187
- }
188
- function daemonLogPath(dir) {
189
- return path.join(dir, "daemon.log");
190
- }
191
- function readDaemonState(dir = defaultStateDir()) {
192
- try {
193
- const raw = readFileSync(daemonStatePath(dir), "utf8");
194
- const data = JSON.parse(raw);
195
- if (typeof data["pid"] !== "number" || !Number.isInteger(data["pid"]) || data["pid"] <= 0 || typeof data["startedAt"] !== "string" || typeof data["any"] !== "boolean" || typeof data["version"] !== "string") {
196
- return null;
197
- }
198
- return { pid: data["pid"], startedAt: data["startedAt"], any: data["any"], version: data["version"] };
199
- } catch {
200
- return null;
201
- }
202
- }
203
- function writeDaemonState(dir, state) {
204
- mkdirSync(dir, { recursive: true });
205
- writeFileSync(daemonStatePath(dir), JSON.stringify(state, null, 2) + "\n", "utf8");
206
- }
207
- function removeDaemonState(dir = defaultStateDir()) {
208
- try {
209
- rmSync(daemonStatePath(dir), { force: true });
210
- } catch {
211
- }
212
- }
213
- function isPidAlive(pid, kill = process.kill) {
214
- try {
215
- kill(pid, 0);
216
- return true;
217
- } catch (err) {
218
- return err.code === "EPERM";
219
- }
220
- }
221
- function daemonStatus(dir = defaultStateDir(), alive = isPidAlive) {
222
- const state = readDaemonState(dir);
223
- if (state === null) return { running: false, state: null };
224
- if (alive(state.pid)) return { running: true, state };
225
- removeDaemonState(dir);
226
- return { running: false, state: null };
227
- }
228
- function defaultSpawn(execPath, scriptPath, args, logPath) {
229
- mkdirSync(path.dirname(logPath), { recursive: true });
230
- const fd = openSync(logPath, "a");
231
- const child = spawn(execPath, [scriptPath, ...args], {
232
- detached: true,
233
- stdio: ["ignore", fd, fd]
234
- });
235
- child.unref();
236
- if (child.pid === void 0) throw new Error("could not spawn daemon child process");
237
- return child.pid;
238
- }
239
- function startDaemon(opts) {
240
- const dir = opts.dir ?? defaultStateDir();
241
- const status = daemonStatus(dir, opts.alive);
242
- if (status.running && status.state !== null) {
243
- return { started: false, reason: `already running (pid ${status.state.pid})` };
244
- }
245
- const execPath = opts.execPath ?? process.execPath;
246
- const scriptPath = opts.scriptPath ?? process.argv[1];
247
- if (scriptPath === void 0) return { started: false, reason: "cannot locate the CLI entry point" };
248
- const args = ["daemon", "run", ...opts.any ? ["--any"] : []];
249
- const spawnProcess = opts.spawnProcess ?? ((a, logPath) => defaultSpawn(execPath, scriptPath, [...a], logPath));
250
- const pid = spawnProcess(args, daemonLogPath(dir));
251
- writeDaemonState(dir, { pid, startedAt: (/* @__PURE__ */ new Date()).toISOString(), any: opts.any, version: opts.version });
252
- return { started: true, pid };
253
- }
254
- async function stopDaemon(opts = {}) {
255
- const dir = opts.dir ?? defaultStateDir();
256
- const kill = opts.kill ?? process.kill;
257
- const alive = opts.alive ?? ((pid) => isPidAlive(pid, kill));
258
- const state = readDaemonState(dir);
259
- if (state === null) return { stopped: false, reason: "not running" };
260
- if (!alive(state.pid)) {
261
- removeDaemonState(dir);
262
- return { stopped: false, reason: "not running (cleaned stale pidfile)" };
263
- }
264
- try {
265
- kill(state.pid, "SIGTERM");
266
- } catch {
267
- }
268
- const deadline = Date.now() + (opts.waitMs ?? 2e3);
269
- while (Date.now() < deadline && alive(state.pid)) {
270
- await new Promise((r) => setTimeout(r, 50));
271
- }
272
- removeDaemonState(dir);
273
- return { stopped: true, pid: state.pid };
274
- }
275
- var DAEMON_SERVICE_LABEL = "ai.vibedating.daemon";
276
- var SYSTEMD_UNIT_NAME = "vibedating.service";
277
- function daemonServicePath(platform = process.platform, homeDir = os.homedir()) {
278
- if (platform === "darwin") {
279
- return path.join(homeDir, "Library", "LaunchAgents", `${DAEMON_SERVICE_LABEL}.plist`);
280
- }
281
- if (platform === "linux") {
282
- return path.join(homeDir, ".config", "systemd", "user", SYSTEMD_UNIT_NAME);
283
- }
284
- return null;
285
- }
286
- function serviceArgv(execPath, scriptPath, any) {
287
- return [execPath, scriptPath, "daemon", "run", ...any ? ["--any"] : []];
288
- }
289
- function renderLaunchdPlist(opts) {
290
- const args = serviceArgv(opts.execPath, opts.scriptPath, opts.any).map((a) => ` <string>${a}</string>`).join("\n");
291
- return `<?xml version="1.0" encoding="UTF-8"?>
292
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
293
- <plist version="1.0">
294
- <dict>
295
- <key>Label</key>
296
- <string>${DAEMON_SERVICE_LABEL}</string>
297
- <key>ProgramArguments</key>
298
- <array>
299
- ${args}
300
- </array>
301
- <key>RunAtLoad</key>
302
- <true/>
303
- <key>KeepAlive</key>
304
- <false/>
305
- <key>StandardOutPath</key>
306
- <string>${opts.logPath}</string>
307
- <key>StandardErrorPath</key>
308
- <string>${opts.logPath}</string>
309
- </dict>
310
- </plist>
311
- `;
312
- }
313
- function renderSystemdUnit(opts) {
314
- const execStart = serviceArgv(opts.execPath, opts.scriptPath, opts.any).join(" ");
315
- return `[Unit]
316
- Description=vibedating notify-only daemon (alerts on new matches; never opens chat/video)
317
-
318
- [Service]
319
- ExecStart=${execStart}
320
- Restart=on-failure
321
- RestartSec=10
322
-
323
- [Install]
324
- WantedBy=default.target
325
- `;
326
- }
327
- var defaultRun = (cmd, args) => spawnSync(cmd, args, { stdio: "inherit" }).status === 0;
328
- function installDaemonService(opts) {
329
- const platform = opts.platform ?? process.platform;
330
- const homeDir = opts.homeDir ?? os.homedir();
331
- const run = opts.run ?? defaultRun;
332
- const servicePath = daemonServicePath(platform, homeDir);
333
- if (servicePath === null) {
334
- return {
335
- installed: false,
336
- servicePath: null,
337
- detail: `unsupported platform (${platform}) \u2014 run \`vibedate daemon start\` manually instead`
338
- };
339
- }
340
- const execPath = opts.execPath ?? process.execPath;
341
- const scriptPath = opts.scriptPath ?? process.argv[1];
342
- if (scriptPath === void 0) {
343
- return { installed: false, servicePath, detail: "cannot locate the CLI entry point" };
344
- }
345
- mkdirSync(path.dirname(servicePath), { recursive: true });
346
- if (platform === "darwin") {
347
- const logPath = daemonLogPath(opts.dir ?? defaultStateDir());
348
- writeFileSync(servicePath, renderLaunchdPlist({ execPath, scriptPath, any: opts.any, logPath }), "utf8");
349
- const uid = process.getuid?.() ?? 501;
350
- run("launchctl", ["bootout", `gui/${uid}`, servicePath]);
351
- if (!run("launchctl", ["bootstrap", `gui/${uid}`, servicePath])) {
352
- return { installed: false, servicePath, detail: "launchctl bootstrap failed \u2014 service written but not loaded" };
353
- }
354
- return { installed: true, servicePath, detail: "launchd agent installed \u2014 the daemon starts on login" };
355
- }
356
- writeFileSync(servicePath, renderSystemdUnit({ execPath, scriptPath, any: opts.any }), "utf8");
357
- run("systemctl", ["--user", "daemon-reload"]);
358
- if (!run("systemctl", ["--user", "enable", "--now", SYSTEMD_UNIT_NAME])) {
359
- return { installed: false, servicePath, detail: "systemctl enable failed \u2014 unit written but not started" };
360
- }
361
- return { installed: true, servicePath, detail: "systemd --user service installed \u2014 the daemon starts on login" };
362
- }
363
- function uninstallDaemonService(opts) {
364
- const platform = opts.platform ?? process.platform;
365
- const homeDir = opts.homeDir ?? os.homedir();
366
- const run = opts.run ?? defaultRun;
367
- const servicePath = daemonServicePath(platform, homeDir);
368
- if (servicePath === null) {
369
- return {
370
- installed: false,
371
- servicePath: null,
372
- detail: `unsupported platform (${platform})`
373
- };
374
- }
375
- if (platform === "darwin") {
376
- const uid = process.getuid?.() ?? 501;
377
- run("launchctl", ["bootout", `gui/${uid}`, servicePath]);
378
- } else {
379
- run("systemctl", ["--user", "disable", "--now", SYSTEMD_UNIT_NAME]);
380
- run("systemctl", ["--user", "daemon-reload"]);
381
- }
382
- try {
383
- rmSync(servicePath, { force: true });
384
- } catch {
385
- }
386
- return { installed: false, servicePath, detail: "login service removed \u2014 the daemon no longer starts on login" };
387
- }
388
-
389
- // src/pairing.ts
390
- var MAX_BUFFERED = 100;
391
- function createPairing() {
392
- const queue = [];
393
- const matchCbs = /* @__PURE__ */ new Set();
394
- const msgCbs = /* @__PURE__ */ new Set();
395
- const queuedCbs = /* @__PURE__ */ new Set();
396
- const buffers = /* @__PURE__ */ new Map();
397
- let current;
398
- const emitMatch = (link) => {
399
- for (const cb of matchCbs) cb(link);
400
- };
401
- const deliver = (from, m) => {
402
- for (const cb of msgCbs) cb(from, m);
403
- };
404
- const notifyQueued = (from, n) => {
405
- for (const cb of queuedCbs) cb(from, n);
406
- };
407
- const flush = (link) => {
408
- const buf = buffers.get(link);
409
- if (buf !== void 0 && buf.length > 0) {
410
- for (const m of buf) deliver(link.hello.handle, m);
411
- buffers.set(link, []);
412
- }
413
- };
414
- const setCurrent = (link) => {
415
- current = link;
416
- if (link !== void 0) flush(link);
417
- emitMatch(link);
418
- };
419
- const watch = (link) => {
420
- link.onClose(() => {
421
- buffers.delete(link);
422
- if (current === link) {
423
- const nextUp = queue.shift();
424
- setCurrent(nextUp);
425
- } else {
426
- const idx = queue.indexOf(link);
427
- if (idx >= 0) queue.splice(idx, 1);
428
- }
429
- });
430
- };
431
- const bindMessages = (link) => {
432
- link.onMessage((m) => {
433
- if (link === current) {
434
- deliver(link.hello.handle, m);
435
- } else {
436
- const buf = buffers.get(link) ?? [];
437
- buf.push(m);
438
- if (buf.length > MAX_BUFFERED) buf.shift();
439
- buffers.set(link, buf);
440
- notifyQueued(link.hello.handle, buf.length);
441
- }
442
- });
443
- };
444
- return {
445
- get available() {
446
- return queue.length;
447
- },
448
- current() {
449
- return current;
450
- },
451
- add(link) {
452
- watch(link);
453
- bindMessages(link);
454
- if (current === void 0) {
455
- setCurrent(link);
456
- } else {
457
- queue.push(link);
458
- }
459
- },
460
- next() {
461
- if (current !== void 0) {
462
- current.close();
463
- current = void 0;
464
- }
465
- const nextUp = queue.shift();
466
- setCurrent(nextUp);
467
- return current;
468
- },
469
- open(handle2) {
470
- const idx = queue.findIndex((l) => l.hello.handle === handle2);
471
- if (idx < 0) return void 0;
472
- const link = queue.splice(idx, 1)[0];
473
- if (current !== void 0) {
474
- current.close();
475
- current = void 0;
476
- }
477
- setCurrent(link);
478
- return current;
479
- },
480
- onMatch(cb) {
481
- matchCbs.add(cb);
482
- },
483
- onMessage(cb) {
484
- msgCbs.add(cb);
485
- },
486
- onQueued(cb) {
487
- queuedCbs.add(cb);
488
- }
489
- };
490
- }
491
-
492
56
  // src/server.ts
493
57
  import http from "http";
494
58
  import { randomUUID } from "crypto";
59
+ import fs from "fs";
60
+ import os from "os";
61
+ import path from "path";
495
62
  import { makeEvent, notify as vibeCoreNotify } from "@pooriaarab/vibe-core";
496
63
 
497
64
  // src/web-app-html.ts
@@ -1035,15 +602,23 @@ var webAppHtml = `<!DOCTYPE html>
1035
602
  max-width: 85%; padding: 7px 11px; border-radius: 13px;
1036
603
  font-size: .82rem; line-height: 1.4; word-break: break-word; white-space: pre-wrap;
1037
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; }
1038
607
  .cp-msg.them{ align-self: flex-start; background: rgba(248,239,232,.07); border: 1px solid var(--border); }
1039
608
  .cp-msg.you{ align-self: flex-end; background: rgba(255,122,104,.16); border: 1px solid rgba(255,122,104,.32); }
1040
609
  .cp-msg.sys{ align-self: center; background: transparent; border: 0; color: var(--muted-2); font-size: .72rem; padding: 2px 6px; }
1041
610
  .chat-panel .cp-inputrow{ display: flex; gap: 8px; padding: 10px; border-top: 1px solid var(--border); }
1042
- .chat-panel .cp-inputrow input{
611
+ .chat-panel .cp-inputrow input[type="text"]{
1043
612
  flex: 1; border: 1px solid var(--border-2); border-radius: 10px; background: rgba(0,0,0,.22);
1044
613
  color: var(--fg); padding: 9px 11px; font: inherit; font-size: .82rem; outline: none; min-width: 0;
1045
614
  }
1046
- .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); }
1047
622
  .chat-panel .cp-send{
1048
623
  border: 0; border-radius: 10px; padding: 9px 14px; font-weight: 700; font-size: .8rem;
1049
624
  background: linear-gradient(180deg, var(--coral), var(--coral-dim)); color: #2a1109;
@@ -1222,6 +797,8 @@ var webAppHtml = `<!DOCTYPE html>
1222
797
  </div>
1223
798
  <div class="cp-msgs" id="chatMsgs"></div>
1224
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;">
1225
802
  <input id="chatInput" type="text" maxlength="4000" placeholder="message&hellip;" autocomplete="off">
1226
803
  <button class="cp-send" id="chatSend" type="button">Send</button>
1227
804
  </div>
@@ -1740,6 +1317,8 @@ var webAppHtml = `<!DOCTYPE html>
1740
1317
  var chatSub = document.getElementById("chatSub");
1741
1318
  var chatMsgs = document.getElementById("chatMsgs");
1742
1319
  var chatInput = document.getElementById("chatInput");
1320
+ var chatAttachBtn = document.getElementById("chatAttachBtn");
1321
+ var chatFileInput = document.getElementById("chatFileInput");
1743
1322
  var chatSend = document.getElementById("chatSend");
1744
1323
  var chatClose = document.getElementById("chatClose");
1745
1324
 
@@ -1747,6 +1326,7 @@ var webAppHtml = `<!DOCTYPE html>
1747
1326
  var conversations = {}; // handle -> [{from:"you"|"them"|"sys", text}]
1748
1327
  var unread = {}; // handle -> count of unseen incoming messages
1749
1328
  var chatLoops = {}; // handle -> true while its poll loop is running
1329
+ var mediaLoops = {}; // handle -> true while its poll loop is running
1750
1330
  var MAX_CHAT_KEPT = 200; // per-conversation local cap (mirrors the server)
1751
1331
 
1752
1332
  function fetchMessage(handle, timeoutMs){
@@ -1757,6 +1337,14 @@ var webAppHtml = `<!DOCTYPE html>
1757
1337
  .catch(function(e){ clearTimeout(t); throw e; });
1758
1338
  }
1759
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
+
1760
1348
  function postChat(handle, text){
1761
1349
  return fetch("/live/message", {
1762
1350
  method: "POST",
@@ -1765,6 +1353,14 @@ var webAppHtml = `<!DOCTYPE html>
1765
1353
  });
1766
1354
  }
1767
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
+
1768
1364
  function peerKnown(handle){
1769
1365
  for (var i = 0; i < knownPeers.length; i++){ if (knownPeers[i].handle === handle) return true; }
1770
1366
  return false;
@@ -1779,27 +1375,51 @@ var webAppHtml = `<!DOCTYPE html>
1779
1375
  // One long-poll loop per connected peer: started when the peer is first
1780
1376
  // seen, stopped when it vanishes (its mailbox on the server is gone).
1781
1377
  function ensureChatLoop(handle){
1782
- if (chatLoops[handle]) return;
1783
- chatLoops[handle] = true;
1784
- (async function(){
1785
- while (peerKnown(handle)) {
1786
- var res;
1787
- try { res = await fetchMessage(handle, 30000); }
1788
- catch(e){ await sleep(1000); continue; }
1789
- if (!res || res.status !== 200) { await sleep(1000); continue; }
1790
- var data = await res.json();
1791
- var m = data && data.message;
1792
- if (!m) continue; // timed out empty
1793
- pushChat(handle, { from: "them", text: m.text });
1794
- if (chatWith === handle) {
1795
- renderChat();
1796
- } else {
1797
- unread[handle] = (unread[handle] || 0) + 1;
1798
- 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
+ }
1799
1396
  }
1800
- }
1801
- delete chatLoops[handle];
1802
- })();
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
+ }
1803
1423
  }
1804
1424
 
1805
1425
  function renderChat(){
@@ -1815,8 +1435,30 @@ var webAppHtml = `<!DOCTYPE html>
1815
1435
  conv.forEach(function(m){
1816
1436
  var el = document.createElement("div");
1817
1437
  el.className = "cp-msg " + (m.from === "you" ? "you" : m.from === "sys" ? "sys" : "them");
1818
- // textContent only \u2014 a peer's text is never parsed as HTML.
1819
- 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
+ }
1820
1462
  chatMsgs.appendChild(el);
1821
1463
  });
1822
1464
  chatMsgs.scrollTop = chatMsgs.scrollHeight;
@@ -1858,6 +1500,34 @@ var webAppHtml = `<!DOCTYPE html>
1858
1500
  chatSend.addEventListener("click", sendChat);
1859
1501
  chatInput.addEventListener("keydown", function(e){ if (e.key === "Enter") sendChat(); });
1860
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
+
1861
1531
  // Render the live-peers rows: one per connected peer with its verification
1862
1532
  // marks, a Chat button (opens the conversation), and a Call button that
1863
1533
  // rings THAT peer specifically.
@@ -2186,7 +1856,7 @@ function createLiveBridge() {
2186
1856
  },
2187
1857
  addLink(link) {
2188
1858
  const handle2 = link.hello.handle;
2189
- boxes.set(handle2, { link, incoming: [], messages: [] });
1859
+ boxes.set(handle2, { link, incoming: [], messages: [], media: [] });
2190
1860
  link.onSignal((f) => {
2191
1861
  const mb = boxes.get(handle2);
2192
1862
  if (mb) mb.incoming.push(f);
@@ -2199,6 +1869,20 @@ function createLiveBridge() {
2199
1869
  mb.messages.splice(0, mb.messages.length - MAX_QUEUED_MESSAGES);
2200
1870
  }
2201
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
+ });
2202
1886
  link.onClose(() => {
2203
1887
  const cur = boxes.get(handle2);
2204
1888
  if (cur && cur.link === link) boxes.delete(handle2);
@@ -2235,6 +1919,39 @@ function createLiveBridge() {
2235
1919
  if (cur.messages.length > 0) return cur.messages.shift() ?? null;
2236
1920
  }
2237
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
+ }
2238
1955
  }
2239
1956
  };
2240
1957
  return bridge;
@@ -2352,10 +2069,18 @@ function send(res, status, contentType, body) {
2352
2069
  function sendJson(res, status, data) {
2353
2070
  send(res, status, "application/json; charset=utf-8", JSON.stringify(data));
2354
2071
  }
2355
- function readBody(req) {
2072
+ function readBody(req, maxLength) {
2356
2073
  return new Promise((resolve, reject) => {
2357
2074
  const chunks = [];
2358
- 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
+ });
2359
2084
  req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
2360
2085
  req.on("error", reject);
2361
2086
  });
@@ -2546,6 +2271,69 @@ async function handle(req, res, opts) {
2546
2271
  sendJson(res, 200, { ok: true });
2547
2272
  return;
2548
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
+ }
2549
2337
  if (req.method === "GET" && pathname === "/api/room") {
2550
2338
  const room = opts.room;
2551
2339
  sendJson(res, 200, room ? { room: room.name, self: room.self ?? null, members: room.members } : { room: null, self: null, members: [] });
@@ -2640,7 +2428,7 @@ async function handle(req, res, opts) {
2640
2428
  }
2641
2429
 
2642
2430
  // src/cli.ts
2643
- var VERSION = "0.7.2";
2431
+ var VERSION = "0.8.1";
2644
2432
  function parsePort(raw) {
2645
2433
  const n = Number(raw);
2646
2434
  if (!Number.isInteger(n) || n < 1 || n > 65535) return void 0;
@@ -3066,6 +2854,16 @@ async function cmdOpen(port, any, room) {
3066
2854
  function shouldKeepAlive(flag, stdinIsTTY) {
3067
2855
  return flag || stdinIsTTY !== true;
3068
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
+ }
3069
2867
  async function cmdLiveViaRelay(profile, to) {
3070
2868
  const target = to !== void 0 ? normalizeHandle(to) : null;
3071
2869
  if (to !== void 0 && target === null) {
@@ -3235,6 +3033,12 @@ async function cmdLive(dating, any, to, keepAlive, viaRelay) {
3235
3033
  process2.stdout.write(` \u2605 found ${sanitizePeerText(link.hello.handle)} \u2014 auto-opening
3236
3034
  `);
3237
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
+ });
3238
3042
  pairing.add(link);
3239
3043
  }
3240
3044
  });
@@ -3242,7 +3046,7 @@ async function cmdLive(dating, any, to, keepAlive, viaRelay) {
3242
3046
  ` topic: ${TOPIC_PREFIX}${profile.league} \u2192 ${session.topic.toString("hex").slice(0, 12)}\u2026${topics.length > 1 ? ` (+${topics.length - 1} more)` : ""}
3243
3047
  `
3244
3048
  );
3245
- 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");
3246
3050
  process2.stdout.write(" video chat: live A/V runs in the web app \u2014 run `vibedating open`\n\n");
3247
3051
  const rl = readline.createInterface({ input: process2.stdin, terminal: false });
3248
3052
  const stop = () => {
@@ -3265,6 +3069,28 @@ async function cmdLive(dating, any, to, keepAlive, viaRelay) {
3265
3069
  const handle2 = text.slice("/open ".length).trim();
3266
3070
  if (pairing.open(handle2) === void 0) {
3267
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)}
3268
3094
  `);
3269
3095
  }
3270
3096
  continue;
@@ -3651,7 +3477,7 @@ Usage:
3651
3477
  --room <name> opens the room view instead: roster +
3652
3478
  group chat + full-mesh group video (~6 people; an SFU
3653
3479
  is the upgrade path for bigger rooms).
3654
- vibedating mcp Run the stdio MCP server (profile, matches)
3480
+ vibedating mcp Run the stdio MCP server (full agent-native tools: live/room/discover/media)
3655
3481
  vibedating --version
3656
3482
  vibedating --help
3657
3483
 
@@ -3739,5 +3565,6 @@ if (entryUrl !== void 0) {
3739
3565
  export {
3740
3566
  formatAgo,
3741
3567
  parseArgs,
3568
+ parseSendCommand,
3742
3569
  shouldKeepAlive
3743
3570
  };