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/chunk-JBADPOR5.js +15748 -0
- package/dist/{chunk-HDHJLZZG.js → chunk-S7MO5OSO.js} +7 -0
- package/dist/cli.d.ts +6 -1
- package/dist/cli.js +305 -478
- package/dist/index.d.ts +4 -319
- package/dist/index.js +1 -1
- package/dist/mcp.d.ts +181 -1
- package/dist/mcp.js +8 -2
- package/dist/room-BStsMaeT.d.ts +320 -0
- package/package.json +1 -1
- package/dist/chunk-ZD6JBWEW.js +0 -63
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
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-
|
|
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…" 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])
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1819
|
-
|
|
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
|
-
|
|
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.
|
|
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 (
|
|
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
|
};
|