vibedate 0.4.0 → 0.5.0
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/README.md +14 -1
- package/dist/{chunk-ZB56ZBAR.js → chunk-I5U3O4RH.js} +1 -1
- package/dist/{chunk-43IYNWES.js → chunk-KKWP4DLY.js} +16 -1
- package/dist/cli.d.ts +18 -3
- package/dist/cli.js +934 -54
- package/dist/index.js +1 -1
- package/dist/mcp.js +2 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
runMcp
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-I5U3O4RH.js";
|
|
5
5
|
import {
|
|
6
6
|
CANDIDATES,
|
|
7
|
+
DEFAULT_HANDLE,
|
|
7
8
|
LIVE_NOTICE,
|
|
9
|
+
MAX_HANDLE_LEN,
|
|
8
10
|
TOPIC_PREFIX,
|
|
9
11
|
addBlock,
|
|
10
12
|
allLeagueNames,
|
|
11
13
|
canShareLive,
|
|
12
14
|
connectProfile,
|
|
15
|
+
defaultStateDir,
|
|
13
16
|
grantLiveConsent,
|
|
14
17
|
isBlocked,
|
|
15
18
|
league,
|
|
@@ -17,6 +20,7 @@ import {
|
|
|
17
20
|
leagueTopic,
|
|
18
21
|
leaguesWithin,
|
|
19
22
|
loadBlocklist,
|
|
23
|
+
loadHandle,
|
|
20
24
|
loadOrCreateIdentity,
|
|
21
25
|
loadPeers,
|
|
22
26
|
loadProfile,
|
|
@@ -27,16 +31,356 @@ import {
|
|
|
27
31
|
removeBlock,
|
|
28
32
|
resolveHandle,
|
|
29
33
|
sameHandle,
|
|
34
|
+
sanitizePeerText,
|
|
30
35
|
saveHandle,
|
|
31
36
|
signHelloClaims,
|
|
32
37
|
startDiscovery
|
|
33
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-KKWP4DLY.js";
|
|
34
39
|
|
|
35
40
|
// src/cli.ts
|
|
36
41
|
import readline from "readline";
|
|
37
42
|
import "url";
|
|
38
43
|
import process2 from "process";
|
|
39
44
|
|
|
45
|
+
// src/handlegen.ts
|
|
46
|
+
import { randomBytes } from "crypto";
|
|
47
|
+
var FIRST = [
|
|
48
|
+
"segfault",
|
|
49
|
+
"yak",
|
|
50
|
+
"vibe",
|
|
51
|
+
"null",
|
|
52
|
+
"async",
|
|
53
|
+
"await",
|
|
54
|
+
"heap",
|
|
55
|
+
"stack",
|
|
56
|
+
"sudo",
|
|
57
|
+
"regex",
|
|
58
|
+
"token",
|
|
59
|
+
"prompt",
|
|
60
|
+
"context",
|
|
61
|
+
"merge",
|
|
62
|
+
"rebase",
|
|
63
|
+
"hotfix",
|
|
64
|
+
"flaky",
|
|
65
|
+
"cursed",
|
|
66
|
+
"based",
|
|
67
|
+
"quantum",
|
|
68
|
+
"turbo",
|
|
69
|
+
"neural",
|
|
70
|
+
"agentic",
|
|
71
|
+
"kernel",
|
|
72
|
+
"docker",
|
|
73
|
+
"kube",
|
|
74
|
+
"lambda",
|
|
75
|
+
"pointer",
|
|
76
|
+
"buffer",
|
|
77
|
+
"packet",
|
|
78
|
+
"syscall",
|
|
79
|
+
"runtime",
|
|
80
|
+
"monad",
|
|
81
|
+
"borrow",
|
|
82
|
+
"cache",
|
|
83
|
+
"deadlock",
|
|
84
|
+
"localhost",
|
|
85
|
+
"darkmode",
|
|
86
|
+
"wasm",
|
|
87
|
+
"diff"
|
|
88
|
+
];
|
|
89
|
+
var SECOND = [
|
|
90
|
+
"sommelier",
|
|
91
|
+
"shaver",
|
|
92
|
+
"goblin",
|
|
93
|
+
"nomad",
|
|
94
|
+
"gremlin",
|
|
95
|
+
"wizard",
|
|
96
|
+
"bard",
|
|
97
|
+
"pirate",
|
|
98
|
+
"ninja",
|
|
99
|
+
"monk",
|
|
100
|
+
"prophet",
|
|
101
|
+
"oracle",
|
|
102
|
+
"smith",
|
|
103
|
+
"farmer",
|
|
104
|
+
"whisperer",
|
|
105
|
+
"tamer",
|
|
106
|
+
"connoisseur",
|
|
107
|
+
"maximalist",
|
|
108
|
+
"enjoyer",
|
|
109
|
+
"dealer",
|
|
110
|
+
"herder",
|
|
111
|
+
"wrangler",
|
|
112
|
+
"juggler",
|
|
113
|
+
"mechanic",
|
|
114
|
+
"surgeon",
|
|
115
|
+
"detective",
|
|
116
|
+
"librarian",
|
|
117
|
+
"alchemist",
|
|
118
|
+
"overlord",
|
|
119
|
+
"apprentice",
|
|
120
|
+
"sensei",
|
|
121
|
+
"chef",
|
|
122
|
+
"dj",
|
|
123
|
+
"ranger",
|
|
124
|
+
"paladin",
|
|
125
|
+
"barbarian",
|
|
126
|
+
"summoner",
|
|
127
|
+
"cartographer",
|
|
128
|
+
"archivist",
|
|
129
|
+
"plumber"
|
|
130
|
+
];
|
|
131
|
+
var SUFFIX = [
|
|
132
|
+
"prime",
|
|
133
|
+
"9000",
|
|
134
|
+
"3000",
|
|
135
|
+
"max",
|
|
136
|
+
"ultra",
|
|
137
|
+
"xl",
|
|
138
|
+
"mk2",
|
|
139
|
+
"777",
|
|
140
|
+
"404",
|
|
141
|
+
"1337",
|
|
142
|
+
"2077",
|
|
143
|
+
"xd"
|
|
144
|
+
];
|
|
145
|
+
function cryptoRand() {
|
|
146
|
+
return randomBytes(4).readUInt32BE(0) / 2 ** 32;
|
|
147
|
+
}
|
|
148
|
+
function pick(list, rand) {
|
|
149
|
+
return list[Math.min(list.length - 1, Math.floor(rand() * list.length))];
|
|
150
|
+
}
|
|
151
|
+
function generateHandle(rand = cryptoRand) {
|
|
152
|
+
const first = pick(FIRST, rand);
|
|
153
|
+
const second = pick(SECOND, rand);
|
|
154
|
+
let body = `${first}_${second}`;
|
|
155
|
+
if (rand() < 0.5) {
|
|
156
|
+
const withSuffix = `${body}_${pick(SUFFIX, rand)}`;
|
|
157
|
+
if (withSuffix.length + 1 <= MAX_HANDLE_LEN) body = withSuffix;
|
|
158
|
+
}
|
|
159
|
+
const canonical = normalizeHandle(body);
|
|
160
|
+
if (canonical === null) throw new Error(`handle generator produced an invalid handle: ${body}`);
|
|
161
|
+
return canonical;
|
|
162
|
+
}
|
|
163
|
+
function ensureHandle(dir = defaultStateDir()) {
|
|
164
|
+
const env = process.env["VIBEDATING_HANDLE"];
|
|
165
|
+
if (env !== void 0 && env.trim() !== "") {
|
|
166
|
+
const canonical = normalizeHandle(env);
|
|
167
|
+
if (canonical !== null) return { handle: canonical, generated: false };
|
|
168
|
+
}
|
|
169
|
+
const persisted = loadHandle(dir);
|
|
170
|
+
if (persisted !== DEFAULT_HANDLE) return { handle: persisted, generated: false };
|
|
171
|
+
const generated = generateHandle();
|
|
172
|
+
return { handle: saveHandle(generated, dir), generated: true };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/daemon.ts
|
|
176
|
+
import { spawn, spawnSync } from "child_process";
|
|
177
|
+
import { mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
178
|
+
import os from "os";
|
|
179
|
+
import path from "path";
|
|
180
|
+
function daemonStatePath(dir) {
|
|
181
|
+
return path.join(dir, "daemon.json");
|
|
182
|
+
}
|
|
183
|
+
function daemonLogPath(dir) {
|
|
184
|
+
return path.join(dir, "daemon.log");
|
|
185
|
+
}
|
|
186
|
+
function readDaemonState(dir = defaultStateDir()) {
|
|
187
|
+
try {
|
|
188
|
+
const raw = readFileSync(daemonStatePath(dir), "utf8");
|
|
189
|
+
const data = JSON.parse(raw);
|
|
190
|
+
if (typeof data["pid"] !== "number" || !Number.isInteger(data["pid"]) || data["pid"] <= 0 || typeof data["startedAt"] !== "string" || typeof data["any"] !== "boolean" || typeof data["version"] !== "string") {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
return { pid: data["pid"], startedAt: data["startedAt"], any: data["any"], version: data["version"] };
|
|
194
|
+
} catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function writeDaemonState(dir, state) {
|
|
199
|
+
mkdirSync(dir, { recursive: true });
|
|
200
|
+
writeFileSync(daemonStatePath(dir), JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
201
|
+
}
|
|
202
|
+
function removeDaemonState(dir = defaultStateDir()) {
|
|
203
|
+
try {
|
|
204
|
+
rmSync(daemonStatePath(dir), { force: true });
|
|
205
|
+
} catch {
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function isPidAlive(pid, kill = process.kill) {
|
|
209
|
+
try {
|
|
210
|
+
kill(pid, 0);
|
|
211
|
+
return true;
|
|
212
|
+
} catch (err) {
|
|
213
|
+
return err.code === "EPERM";
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function daemonStatus(dir = defaultStateDir(), alive = isPidAlive) {
|
|
217
|
+
const state = readDaemonState(dir);
|
|
218
|
+
if (state === null) return { running: false, state: null };
|
|
219
|
+
if (alive(state.pid)) return { running: true, state };
|
|
220
|
+
removeDaemonState(dir);
|
|
221
|
+
return { running: false, state: null };
|
|
222
|
+
}
|
|
223
|
+
function defaultSpawn(execPath, scriptPath, args, logPath) {
|
|
224
|
+
mkdirSync(path.dirname(logPath), { recursive: true });
|
|
225
|
+
const fd = openSync(logPath, "a");
|
|
226
|
+
const child = spawn(execPath, [scriptPath, ...args], {
|
|
227
|
+
detached: true,
|
|
228
|
+
stdio: ["ignore", fd, fd]
|
|
229
|
+
});
|
|
230
|
+
child.unref();
|
|
231
|
+
if (child.pid === void 0) throw new Error("could not spawn daemon child process");
|
|
232
|
+
return child.pid;
|
|
233
|
+
}
|
|
234
|
+
function startDaemon(opts) {
|
|
235
|
+
const dir = opts.dir ?? defaultStateDir();
|
|
236
|
+
const status = daemonStatus(dir, opts.alive);
|
|
237
|
+
if (status.running && status.state !== null) {
|
|
238
|
+
return { started: false, reason: `already running (pid ${status.state.pid})` };
|
|
239
|
+
}
|
|
240
|
+
const execPath = opts.execPath ?? process.execPath;
|
|
241
|
+
const scriptPath = opts.scriptPath ?? process.argv[1];
|
|
242
|
+
if (scriptPath === void 0) return { started: false, reason: "cannot locate the CLI entry point" };
|
|
243
|
+
const args = ["daemon", "run", ...opts.any ? ["--any"] : []];
|
|
244
|
+
const spawnProcess = opts.spawnProcess ?? ((a, logPath) => defaultSpawn(execPath, scriptPath, [...a], logPath));
|
|
245
|
+
const pid = spawnProcess(args, daemonLogPath(dir));
|
|
246
|
+
writeDaemonState(dir, { pid, startedAt: (/* @__PURE__ */ new Date()).toISOString(), any: opts.any, version: opts.version });
|
|
247
|
+
return { started: true, pid };
|
|
248
|
+
}
|
|
249
|
+
async function stopDaemon(opts = {}) {
|
|
250
|
+
const dir = opts.dir ?? defaultStateDir();
|
|
251
|
+
const kill = opts.kill ?? process.kill;
|
|
252
|
+
const alive = opts.alive ?? ((pid) => isPidAlive(pid, kill));
|
|
253
|
+
const state = readDaemonState(dir);
|
|
254
|
+
if (state === null) return { stopped: false, reason: "not running" };
|
|
255
|
+
if (!alive(state.pid)) {
|
|
256
|
+
removeDaemonState(dir);
|
|
257
|
+
return { stopped: false, reason: "not running (cleaned stale pidfile)" };
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
kill(state.pid, "SIGTERM");
|
|
261
|
+
} catch {
|
|
262
|
+
}
|
|
263
|
+
const deadline = Date.now() + (opts.waitMs ?? 2e3);
|
|
264
|
+
while (Date.now() < deadline && alive(state.pid)) {
|
|
265
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
266
|
+
}
|
|
267
|
+
removeDaemonState(dir);
|
|
268
|
+
return { stopped: true, pid: state.pid };
|
|
269
|
+
}
|
|
270
|
+
var DAEMON_SERVICE_LABEL = "ai.vibedating.daemon";
|
|
271
|
+
var SYSTEMD_UNIT_NAME = "vibedating.service";
|
|
272
|
+
function daemonServicePath(platform = process.platform, homeDir = os.homedir()) {
|
|
273
|
+
if (platform === "darwin") {
|
|
274
|
+
return path.join(homeDir, "Library", "LaunchAgents", `${DAEMON_SERVICE_LABEL}.plist`);
|
|
275
|
+
}
|
|
276
|
+
if (platform === "linux") {
|
|
277
|
+
return path.join(homeDir, ".config", "systemd", "user", SYSTEMD_UNIT_NAME);
|
|
278
|
+
}
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
function serviceArgv(execPath, scriptPath, any) {
|
|
282
|
+
return [execPath, scriptPath, "daemon", "run", ...any ? ["--any"] : []];
|
|
283
|
+
}
|
|
284
|
+
function renderLaunchdPlist(opts) {
|
|
285
|
+
const args = serviceArgv(opts.execPath, opts.scriptPath, opts.any).map((a) => ` <string>${a}</string>`).join("\n");
|
|
286
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
287
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
288
|
+
<plist version="1.0">
|
|
289
|
+
<dict>
|
|
290
|
+
<key>Label</key>
|
|
291
|
+
<string>${DAEMON_SERVICE_LABEL}</string>
|
|
292
|
+
<key>ProgramArguments</key>
|
|
293
|
+
<array>
|
|
294
|
+
${args}
|
|
295
|
+
</array>
|
|
296
|
+
<key>RunAtLoad</key>
|
|
297
|
+
<true/>
|
|
298
|
+
<key>KeepAlive</key>
|
|
299
|
+
<false/>
|
|
300
|
+
<key>StandardOutPath</key>
|
|
301
|
+
<string>${opts.logPath}</string>
|
|
302
|
+
<key>StandardErrorPath</key>
|
|
303
|
+
<string>${opts.logPath}</string>
|
|
304
|
+
</dict>
|
|
305
|
+
</plist>
|
|
306
|
+
`;
|
|
307
|
+
}
|
|
308
|
+
function renderSystemdUnit(opts) {
|
|
309
|
+
const execStart = serviceArgv(opts.execPath, opts.scriptPath, opts.any).join(" ");
|
|
310
|
+
return `[Unit]
|
|
311
|
+
Description=vibedating notify-only daemon (alerts on new matches; never opens chat/video)
|
|
312
|
+
|
|
313
|
+
[Service]
|
|
314
|
+
ExecStart=${execStart}
|
|
315
|
+
Restart=on-failure
|
|
316
|
+
RestartSec=10
|
|
317
|
+
|
|
318
|
+
[Install]
|
|
319
|
+
WantedBy=default.target
|
|
320
|
+
`;
|
|
321
|
+
}
|
|
322
|
+
var defaultRun = (cmd, args) => spawnSync(cmd, args, { stdio: "inherit" }).status === 0;
|
|
323
|
+
function installDaemonService(opts) {
|
|
324
|
+
const platform = opts.platform ?? process.platform;
|
|
325
|
+
const homeDir = opts.homeDir ?? os.homedir();
|
|
326
|
+
const run = opts.run ?? defaultRun;
|
|
327
|
+
const servicePath = daemonServicePath(platform, homeDir);
|
|
328
|
+
if (servicePath === null) {
|
|
329
|
+
return {
|
|
330
|
+
installed: false,
|
|
331
|
+
servicePath: null,
|
|
332
|
+
detail: `unsupported platform (${platform}) \u2014 run \`vibedate daemon start\` manually instead`
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
const execPath = opts.execPath ?? process.execPath;
|
|
336
|
+
const scriptPath = opts.scriptPath ?? process.argv[1];
|
|
337
|
+
if (scriptPath === void 0) {
|
|
338
|
+
return { installed: false, servicePath, detail: "cannot locate the CLI entry point" };
|
|
339
|
+
}
|
|
340
|
+
mkdirSync(path.dirname(servicePath), { recursive: true });
|
|
341
|
+
if (platform === "darwin") {
|
|
342
|
+
const logPath = daemonLogPath(opts.dir ?? defaultStateDir());
|
|
343
|
+
writeFileSync(servicePath, renderLaunchdPlist({ execPath, scriptPath, any: opts.any, logPath }), "utf8");
|
|
344
|
+
const uid = process.getuid?.() ?? 501;
|
|
345
|
+
run("launchctl", ["bootout", `gui/${uid}`, servicePath]);
|
|
346
|
+
if (!run("launchctl", ["bootstrap", `gui/${uid}`, servicePath])) {
|
|
347
|
+
return { installed: false, servicePath, detail: "launchctl bootstrap failed \u2014 service written but not loaded" };
|
|
348
|
+
}
|
|
349
|
+
return { installed: true, servicePath, detail: "launchd agent installed \u2014 the daemon starts on login" };
|
|
350
|
+
}
|
|
351
|
+
writeFileSync(servicePath, renderSystemdUnit({ execPath, scriptPath, any: opts.any }), "utf8");
|
|
352
|
+
run("systemctl", ["--user", "daemon-reload"]);
|
|
353
|
+
if (!run("systemctl", ["--user", "enable", "--now", SYSTEMD_UNIT_NAME])) {
|
|
354
|
+
return { installed: false, servicePath, detail: "systemctl enable failed \u2014 unit written but not started" };
|
|
355
|
+
}
|
|
356
|
+
return { installed: true, servicePath, detail: "systemd --user service installed \u2014 the daemon starts on login" };
|
|
357
|
+
}
|
|
358
|
+
function uninstallDaemonService(opts) {
|
|
359
|
+
const platform = opts.platform ?? process.platform;
|
|
360
|
+
const homeDir = opts.homeDir ?? os.homedir();
|
|
361
|
+
const run = opts.run ?? defaultRun;
|
|
362
|
+
const servicePath = daemonServicePath(platform, homeDir);
|
|
363
|
+
if (servicePath === null) {
|
|
364
|
+
return {
|
|
365
|
+
installed: false,
|
|
366
|
+
servicePath: null,
|
|
367
|
+
detail: `unsupported platform (${platform})`
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
if (platform === "darwin") {
|
|
371
|
+
const uid = process.getuid?.() ?? 501;
|
|
372
|
+
run("launchctl", ["bootout", `gui/${uid}`, servicePath]);
|
|
373
|
+
} else {
|
|
374
|
+
run("systemctl", ["--user", "disable", "--now", SYSTEMD_UNIT_NAME]);
|
|
375
|
+
run("systemctl", ["--user", "daemon-reload"]);
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
rmSync(servicePath, { force: true });
|
|
379
|
+
} catch {
|
|
380
|
+
}
|
|
381
|
+
return { installed: false, servicePath, detail: "login service removed \u2014 the daemon no longer starts on login" };
|
|
382
|
+
}
|
|
383
|
+
|
|
40
384
|
// src/pairing.ts
|
|
41
385
|
function createPairing() {
|
|
42
386
|
const queue = [];
|
|
@@ -110,6 +454,7 @@ function createPairing() {
|
|
|
110
454
|
|
|
111
455
|
// src/server.ts
|
|
112
456
|
import http from "http";
|
|
457
|
+
import { randomUUID } from "crypto";
|
|
113
458
|
import { makeEvent, notify as vibeCoreNotify } from "@pooriaarab/vibe-core";
|
|
114
459
|
|
|
115
460
|
// src/web-app-html.ts
|
|
@@ -545,6 +890,80 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
545
890
|
background: var(--danger); color: #2a0a0c;
|
|
546
891
|
}
|
|
547
892
|
.video-modal .vhangup:hover{ filter: brightness(1.06); }
|
|
893
|
+
.live-panel .lp-legend{ font-size: .66rem; color: var(--muted-2); margin-top: 8px; line-height: 1.4; }
|
|
894
|
+
.incoming-call{
|
|
895
|
+
position: fixed; inset: 0; z-index: 65;
|
|
896
|
+
display: flex; align-items: center; justify-content: center;
|
|
897
|
+
background: rgba(12,7,15,.6); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
|
|
898
|
+
opacity: 0; pointer-events: none; transition: opacity var(--dur-med) ease;
|
|
899
|
+
}
|
|
900
|
+
.incoming-call.is-open{ opacity: 1; pointer-events: auto; }
|
|
901
|
+
.incoming-call .ic-card{
|
|
902
|
+
background: linear-gradient(165deg, var(--bg-card), var(--bg-card-2));
|
|
903
|
+
border: 1px solid var(--border-2); border-radius: 24px; box-shadow: var(--shadow-3);
|
|
904
|
+
padding: 30px 28px; max-width: 320px; text-align: center;
|
|
905
|
+
transform: scale(.85) translateY(10px); transition: transform var(--dur-med) var(--ease-bounce);
|
|
906
|
+
}
|
|
907
|
+
.incoming-call.is-open .ic-card{ transform: scale(1) translateY(0); }
|
|
908
|
+
.incoming-call h3{ margin: 0 0 6px; font-size: 1.15rem; font-weight: 800; word-break: break-all; }
|
|
909
|
+
.incoming-call p{ color: var(--muted); font-size: .85rem; margin: 0 0 18px; }
|
|
910
|
+
.incoming-call .ic-actions{ display: flex; gap: 10px; }
|
|
911
|
+
|
|
912
|
+
/* ---- live text chat (browser <-> PeerLink over /live/message) ---- */
|
|
913
|
+
.live-actions{ display: flex; gap: 6px; flex-shrink: 0; }
|
|
914
|
+
.live-row .cbtn{
|
|
915
|
+
border: 1px solid var(--border-2); border-radius: 9px; padding: 7px 12px;
|
|
916
|
+
font-weight: 700; font-size: .76rem; background: transparent; color: var(--muted);
|
|
917
|
+
transition: color var(--dur-fast) ease, border-color var(--dur-fast) ease, transform var(--dur-fast) var(--ease-out);
|
|
918
|
+
flex-shrink: 0;
|
|
919
|
+
}
|
|
920
|
+
.live-row .cbtn:hover{ color: var(--fg); border-color: var(--muted-2); }
|
|
921
|
+
.live-row .cbtn:active{ transform: scale(.95); }
|
|
922
|
+
.live-row .cbtn.has-unread{ color: var(--coral); border-color: var(--coral); }
|
|
923
|
+
.chat-panel{
|
|
924
|
+
position: fixed; right: 18px; bottom: 18px; z-index: 45;
|
|
925
|
+
width: min(320px, calc(100vw - 36px));
|
|
926
|
+
background: linear-gradient(180deg, var(--bg-card), var(--bg-card-2));
|
|
927
|
+
border: 1px solid var(--border-2); border-radius: 16px; box-shadow: var(--shadow-2);
|
|
928
|
+
display: none; flex-direction: column; overflow: hidden;
|
|
929
|
+
}
|
|
930
|
+
.chat-panel.is-open{ display: flex; animation: rise var(--dur-med) var(--ease-out); }
|
|
931
|
+
.chat-panel .cp-head{
|
|
932
|
+
display: flex; align-items: center; justify-content: space-between; gap: 10px;
|
|
933
|
+
padding: 11px 14px; border-bottom: 1px solid var(--border);
|
|
934
|
+
}
|
|
935
|
+
.chat-panel .cp-title{ font-size: .84rem; font-weight: 700; word-break: break-all; }
|
|
936
|
+
.chat-panel .cp-sub{ font-size: .68rem; color: var(--muted-2); margin-top: 2px; }
|
|
937
|
+
.chat-panel .cp-close{
|
|
938
|
+
border: 0; background: transparent; color: var(--muted-2);
|
|
939
|
+
font-size: 1.15rem; line-height: 1; padding: 4px; flex-shrink: 0;
|
|
940
|
+
}
|
|
941
|
+
.chat-panel .cp-close:hover{ color: var(--fg); }
|
|
942
|
+
.chat-panel .cp-msgs{
|
|
943
|
+
padding: 12px 14px; min-height: 120px; max-height: 260px; overflow-y: auto;
|
|
944
|
+
display: flex; flex-direction: column; gap: 7px;
|
|
945
|
+
}
|
|
946
|
+
.chat-panel .cp-empty{ font-size: .76rem; color: var(--muted-2); text-align: center; margin: auto; }
|
|
947
|
+
.cp-msg{
|
|
948
|
+
max-width: 85%; padding: 7px 11px; border-radius: 13px;
|
|
949
|
+
font-size: .82rem; line-height: 1.4; word-break: break-word; white-space: pre-wrap;
|
|
950
|
+
}
|
|
951
|
+
.cp-msg.them{ align-self: flex-start; background: rgba(248,239,232,.07); border: 1px solid var(--border); }
|
|
952
|
+
.cp-msg.you{ align-self: flex-end; background: rgba(255,122,104,.16); border: 1px solid rgba(255,122,104,.32); }
|
|
953
|
+
.cp-msg.sys{ align-self: center; background: transparent; border: 0; color: var(--muted-2); font-size: .72rem; padding: 2px 6px; }
|
|
954
|
+
.chat-panel .cp-inputrow{ display: flex; gap: 8px; padding: 10px; border-top: 1px solid var(--border); }
|
|
955
|
+
.chat-panel .cp-inputrow input{
|
|
956
|
+
flex: 1; border: 1px solid var(--border-2); border-radius: 10px; background: rgba(0,0,0,.22);
|
|
957
|
+
color: var(--fg); padding: 9px 11px; font: inherit; font-size: .82rem; outline: none; min-width: 0;
|
|
958
|
+
}
|
|
959
|
+
.chat-panel .cp-inputrow input:focus{ border-color: var(--coral); }
|
|
960
|
+
.chat-panel .cp-send{
|
|
961
|
+
border: 0; border-radius: 10px; padding: 9px 14px; font-weight: 700; font-size: .8rem;
|
|
962
|
+
background: linear-gradient(180deg, var(--coral), var(--coral-dim)); color: #2a1109;
|
|
963
|
+
transition: filter var(--dur-fast) ease, transform var(--dur-fast) var(--ease-out);
|
|
964
|
+
}
|
|
965
|
+
.chat-panel .cp-send:hover{ filter: brightness(1.06); }
|
|
966
|
+
.chat-panel .cp-send:active{ transform: scale(.95); }
|
|
548
967
|
</style>
|
|
549
968
|
</head>
|
|
550
969
|
<body>
|
|
@@ -689,9 +1108,36 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
689
1108
|
</div>
|
|
690
1109
|
</div>
|
|
691
1110
|
|
|
692
|
-
<aside class="live-panel" id="livePanel" aria-label="Live
|
|
693
|
-
<div class="lp-title"><span class="lp-dot" aria-hidden="true"></span> Live
|
|
1111
|
+
<aside class="live-panel" id="livePanel" aria-label="Live peers">
|
|
1112
|
+
<div class="lp-title"><span class="lp-dot" aria-hidden="true"></span> Live peers</div>
|
|
694
1113
|
<div id="liveRows"></div>
|
|
1114
|
+
<div class="lp-legend">✓ usage verified · 🔑 identity verified — call someone, or wait for a call</div>
|
|
1115
|
+
</aside>
|
|
1116
|
+
|
|
1117
|
+
<div class="incoming-call" id="incomingCall" role="dialog" aria-modal="true" aria-label="Incoming call">
|
|
1118
|
+
<div class="ic-card">
|
|
1119
|
+
<h3 id="incomingCaller">@peer</h3>
|
|
1120
|
+
<p>is calling you — video chat</p>
|
|
1121
|
+
<div class="ic-actions">
|
|
1122
|
+
<button class="btn btn-ghost" id="declineBtn" type="button">Decline</button>
|
|
1123
|
+
<button class="btn btn-primary" id="acceptBtn" type="button">Accept</button>
|
|
1124
|
+
</div>
|
|
1125
|
+
</div>
|
|
1126
|
+
</div>
|
|
1127
|
+
|
|
1128
|
+
<aside class="chat-panel" id="chatPanel" aria-label="Text chat">
|
|
1129
|
+
<div class="cp-head">
|
|
1130
|
+
<div>
|
|
1131
|
+
<div class="cp-title" id="chatTitle">@peer</div>
|
|
1132
|
+
<div class="cp-sub" id="chatSub">live over the P2P link · text only</div>
|
|
1133
|
+
</div>
|
|
1134
|
+
<button class="cp-close" id="chatClose" type="button" aria-label="Close chat">×</button>
|
|
1135
|
+
</div>
|
|
1136
|
+
<div class="cp-msgs" id="chatMsgs"></div>
|
|
1137
|
+
<div class="cp-inputrow">
|
|
1138
|
+
<input id="chatInput" type="text" maxlength="4000" placeholder="message…" autocomplete="off">
|
|
1139
|
+
<button class="cp-send" id="chatSend" type="button">Send</button>
|
|
1140
|
+
</div>
|
|
695
1141
|
</aside>
|
|
696
1142
|
|
|
697
1143
|
<div class="video-modal" id="videoModal" role="dialog" aria-modal="true" aria-label="Video call">
|
|
@@ -998,9 +1444,15 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
998
1444
|
var remoteVideo = document.getElementById("remoteVideo");
|
|
999
1445
|
var remoteLabel = document.getElementById("remoteLabel");
|
|
1000
1446
|
var hangupBtn = document.getElementById("hangupBtn");
|
|
1447
|
+
var incomingCallEl = document.getElementById("incomingCall");
|
|
1448
|
+
var incomingCaller = document.getElementById("incomingCaller");
|
|
1449
|
+
var acceptBtn = document.getElementById("acceptBtn");
|
|
1450
|
+
var declineBtn = document.getElementById("declineBtn");
|
|
1001
1451
|
|
|
1002
1452
|
// One in-flight call's state. remoteHandle === null means idle.
|
|
1003
1453
|
var rtc = { pc: null, localStream: null, remoteHandle: null, role: null };
|
|
1454
|
+
// An incoming offer awaiting the user's accept/decline (never auto-answered).
|
|
1455
|
+
var pendingOffer = null;
|
|
1004
1456
|
var knownPeers = [];
|
|
1005
1457
|
var idleIdx = 0;
|
|
1006
1458
|
|
|
@@ -1117,7 +1569,8 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
1117
1569
|
}
|
|
1118
1570
|
|
|
1119
1571
|
// Idle listener: when not in a call, watch known peers round-robin so an
|
|
1120
|
-
// incoming offer (the peer clicked THEIR
|
|
1572
|
+
// incoming offer (the peer clicked THEIR Call button) is noticed. Offers are
|
|
1573
|
+
// never auto-answered \u2014 the caller is named and the user accepts or declines.
|
|
1121
1574
|
async function idleLoop(){
|
|
1122
1575
|
while (true) {
|
|
1123
1576
|
if (rtc.remoteHandle || knownPeers.length === 0) { await sleep(1000); continue; }
|
|
@@ -1129,7 +1582,12 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
1129
1582
|
var data = await res.json();
|
|
1130
1583
|
var f = data && data.frame;
|
|
1131
1584
|
if (f && f.t === "rtc-offer" && !rtc.pc && !rtc.remoteHandle) {
|
|
1132
|
-
|
|
1585
|
+
// One prompt at a time: a re-offer from the SAME caller refreshes
|
|
1586
|
+
// the stored sdp; an offer from someone else while a prompt is up
|
|
1587
|
+
// is dropped (their call simply fails to connect).
|
|
1588
|
+
if (pendingOffer && pendingOffer.handle !== peer.handle) continue;
|
|
1589
|
+
pendingOffer = { handle: peer.handle, sdp: f.sdp };
|
|
1590
|
+
showIncomingPrompt(peer.handle);
|
|
1133
1591
|
}
|
|
1134
1592
|
}
|
|
1135
1593
|
} catch(e){ /* abort/timeout \u2014 loop to next peer */ }
|
|
@@ -1137,6 +1595,22 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
1137
1595
|
}
|
|
1138
1596
|
idleLoop();
|
|
1139
1597
|
|
|
1598
|
+
function showIncomingPrompt(handle){
|
|
1599
|
+
incomingCaller.textContent = handle;
|
|
1600
|
+
incomingCallEl.classList.add("is-open");
|
|
1601
|
+
}
|
|
1602
|
+
function clearIncomingPrompt(){
|
|
1603
|
+
pendingOffer = null;
|
|
1604
|
+
incomingCallEl.classList.remove("is-open");
|
|
1605
|
+
}
|
|
1606
|
+
acceptBtn.addEventListener("click", function(){
|
|
1607
|
+
var p = pendingOffer;
|
|
1608
|
+
clearIncomingPrompt();
|
|
1609
|
+
if (!p) return;
|
|
1610
|
+
answerIncomingOffer(p.handle, p.sdp).catch(function(){ hangup(); });
|
|
1611
|
+
});
|
|
1612
|
+
declineBtn.addEventListener("click", clearIncomingPrompt);
|
|
1613
|
+
|
|
1140
1614
|
function hangup(){
|
|
1141
1615
|
try { if (rtc.pc) rtc.pc.close(); } catch(e){}
|
|
1142
1616
|
rtc.pc = null;
|
|
@@ -1152,7 +1626,171 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
1152
1626
|
}
|
|
1153
1627
|
hangupBtn.addEventListener("click", hangup);
|
|
1154
1628
|
|
|
1155
|
-
|
|
1629
|
+
/* ---- Live text chat: msg frames over the same PeerLinks, via /live/message ----
|
|
1630
|
+
* One long-poll loop per connected peer surfaces incoming texts immediately,
|
|
1631
|
+
* no matter which conversation is open. Outbound texts POST {handle, text};
|
|
1632
|
+
* the server re-validates through the frame allowlist and mints id/at. */
|
|
1633
|
+
var chatPanel = document.getElementById("chatPanel");
|
|
1634
|
+
var chatTitle = document.getElementById("chatTitle");
|
|
1635
|
+
var chatSub = document.getElementById("chatSub");
|
|
1636
|
+
var chatMsgs = document.getElementById("chatMsgs");
|
|
1637
|
+
var chatInput = document.getElementById("chatInput");
|
|
1638
|
+
var chatSend = document.getElementById("chatSend");
|
|
1639
|
+
var chatClose = document.getElementById("chatClose");
|
|
1640
|
+
|
|
1641
|
+
var chatWith = null; // handle of the open conversation (null = closed)
|
|
1642
|
+
var conversations = {}; // handle -> [{from:"you"|"them"|"sys", text}]
|
|
1643
|
+
var unread = {}; // handle -> count of unseen incoming messages
|
|
1644
|
+
var chatLoops = {}; // handle -> true while its poll loop is running
|
|
1645
|
+
var MAX_CHAT_KEPT = 200; // per-conversation local cap (mirrors the server)
|
|
1646
|
+
|
|
1647
|
+
function fetchMessage(handle, timeoutMs){
|
|
1648
|
+
var ctrl = new AbortController();
|
|
1649
|
+
var t = setTimeout(function(){ ctrl.abort(); }, timeoutMs);
|
|
1650
|
+
return fetch("/live/message?handle=" + encodeURIComponent(handle), { signal: ctrl.signal })
|
|
1651
|
+
.then(function(r){ clearTimeout(t); return r; })
|
|
1652
|
+
.catch(function(e){ clearTimeout(t); throw e; });
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
function postChat(handle, text){
|
|
1656
|
+
return fetch("/live/message", {
|
|
1657
|
+
method: "POST",
|
|
1658
|
+
headers: { "content-type": "application/json" },
|
|
1659
|
+
body: JSON.stringify({ handle: handle, text: text })
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
function peerKnown(handle){
|
|
1664
|
+
for (var i = 0; i < knownPeers.length; i++){ if (knownPeers[i].handle === handle) return true; }
|
|
1665
|
+
return false;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
function pushChat(handle, entry){
|
|
1669
|
+
var conv = conversations[handle] || (conversations[handle] = []);
|
|
1670
|
+
conv.push(entry);
|
|
1671
|
+
if (conv.length > MAX_CHAT_KEPT) conv.splice(0, conv.length - MAX_CHAT_KEPT);
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
// One long-poll loop per connected peer: started when the peer is first
|
|
1675
|
+
// seen, stopped when it vanishes (its mailbox on the server is gone).
|
|
1676
|
+
function ensureChatLoop(handle){
|
|
1677
|
+
if (chatLoops[handle]) return;
|
|
1678
|
+
chatLoops[handle] = true;
|
|
1679
|
+
(async function(){
|
|
1680
|
+
while (peerKnown(handle)) {
|
|
1681
|
+
var res;
|
|
1682
|
+
try { res = await fetchMessage(handle, 30000); }
|
|
1683
|
+
catch(e){ await sleep(1000); continue; }
|
|
1684
|
+
if (!res || res.status !== 200) { await sleep(1000); continue; }
|
|
1685
|
+
var data = await res.json();
|
|
1686
|
+
var m = data && data.message;
|
|
1687
|
+
if (!m) continue; // timed out empty
|
|
1688
|
+
pushChat(handle, { from: "them", text: m.text });
|
|
1689
|
+
if (chatWith === handle) {
|
|
1690
|
+
renderChat();
|
|
1691
|
+
} else {
|
|
1692
|
+
unread[handle] = (unread[handle] || 0) + 1;
|
|
1693
|
+
renderLiveRows(knownPeers);
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
delete chatLoops[handle];
|
|
1697
|
+
})();
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
function renderChat(){
|
|
1701
|
+
chatMsgs.innerHTML = "";
|
|
1702
|
+
var conv = (chatWith && conversations[chatWith]) || [];
|
|
1703
|
+
if (conv.length === 0) {
|
|
1704
|
+
var empty = document.createElement("div");
|
|
1705
|
+
empty.className = "cp-empty";
|
|
1706
|
+
empty.textContent = "No messages yet \u2014 say hi.";
|
|
1707
|
+
chatMsgs.appendChild(empty);
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
conv.forEach(function(m){
|
|
1711
|
+
var el = document.createElement("div");
|
|
1712
|
+
el.className = "cp-msg " + (m.from === "you" ? "you" : m.from === "sys" ? "sys" : "them");
|
|
1713
|
+
// textContent only \u2014 a peer's text is never parsed as HTML.
|
|
1714
|
+
el.textContent = m.text;
|
|
1715
|
+
chatMsgs.appendChild(el);
|
|
1716
|
+
});
|
|
1717
|
+
chatMsgs.scrollTop = chatMsgs.scrollHeight;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
function openChat(handle){
|
|
1721
|
+
chatWith = handle;
|
|
1722
|
+
delete unread[handle];
|
|
1723
|
+
chatTitle.textContent = handle;
|
|
1724
|
+
chatSub.textContent = peerKnown(handle)
|
|
1725
|
+
? "live over the P2P link \xB7 text only"
|
|
1726
|
+
: "peer offline \xB7 messages will not deliver";
|
|
1727
|
+
chatPanel.classList.add("is-open");
|
|
1728
|
+
renderChat();
|
|
1729
|
+
renderLiveRows(knownPeers);
|
|
1730
|
+
chatInput.focus();
|
|
1731
|
+
}
|
|
1732
|
+
chatClose.addEventListener("click", function(){
|
|
1733
|
+
chatWith = null;
|
|
1734
|
+
chatPanel.classList.remove("is-open");
|
|
1735
|
+
});
|
|
1736
|
+
|
|
1737
|
+
function sendChat(){
|
|
1738
|
+
var target = chatWith;
|
|
1739
|
+
var text = chatInput.value.trim();
|
|
1740
|
+
if (!target || text === "") return;
|
|
1741
|
+
chatInput.value = "";
|
|
1742
|
+
postChat(target, text).then(function(res){
|
|
1743
|
+
// Honest failure \u2014 never show a message as sent when it wasn't.
|
|
1744
|
+
pushChat(target, res && res.status === 200
|
|
1745
|
+
? { from: "you", text: text }
|
|
1746
|
+
: { from: "sys", text: "(not delivered \u2014 peer offline?)" });
|
|
1747
|
+
if (chatWith === target) renderChat();
|
|
1748
|
+
}).catch(function(){
|
|
1749
|
+
pushChat(target, { from: "sys", text: "(not delivered \u2014 server unreachable)" });
|
|
1750
|
+
if (chatWith === target) renderChat();
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
chatSend.addEventListener("click", sendChat);
|
|
1754
|
+
chatInput.addEventListener("keydown", function(e){ if (e.key === "Enter") sendChat(); });
|
|
1755
|
+
|
|
1756
|
+
// Render the live-peers rows: one per connected peer with its verification
|
|
1757
|
+
// marks, a Chat button (opens the conversation), and a Call button that
|
|
1758
|
+
// rings THAT peer specifically.
|
|
1759
|
+
function renderLiveRows(peers){
|
|
1760
|
+
liveRows.innerHTML = "";
|
|
1761
|
+
peers.forEach(function(p){
|
|
1762
|
+
var row = document.createElement("div");
|
|
1763
|
+
row.className = "live-row";
|
|
1764
|
+
var who = document.createElement("div");
|
|
1765
|
+
// Marks from the peer's hello, shown only when present: \u2713 usage
|
|
1766
|
+
// verified (real local logs), \u{1F511} identity-verified (signed hello).
|
|
1767
|
+
var marks = (p.verified === true ? " \u2713" : "") + (p.identityVerified === true ? " \u{1F511}" : "");
|
|
1768
|
+
var h = document.createElement("div"); h.className = "h"; h.textContent = p.handle + marks;
|
|
1769
|
+
var s = document.createElement("div"); s.className = "s"; s.textContent = p.league + " \xB7 " + p.harness;
|
|
1770
|
+
who.appendChild(h); who.appendChild(s);
|
|
1771
|
+
var actions = document.createElement("div");
|
|
1772
|
+
actions.className = "live-actions";
|
|
1773
|
+
var chatBtn = document.createElement("button");
|
|
1774
|
+
chatBtn.className = "cbtn" + (unread[p.handle] ? " has-unread" : "");
|
|
1775
|
+
chatBtn.type = "button";
|
|
1776
|
+
chatBtn.textContent = "Chat" + (unread[p.handle] ? " (" + unread[p.handle] + ")" : "");
|
|
1777
|
+
chatBtn.addEventListener("click", function(){ openChat(p.handle); });
|
|
1778
|
+
var btn = document.createElement("button");
|
|
1779
|
+
btn.className = "vbtn"; btn.type = "button"; btn.textContent = "Call";
|
|
1780
|
+
btn.addEventListener("click", function(){
|
|
1781
|
+
if (rtc.remoteHandle || pendingOffer) return; // already in a call / prompt
|
|
1782
|
+
btn.disabled = true;
|
|
1783
|
+
// DIRECTED call: the rtc-offer is relayed only to this peer's handle.
|
|
1784
|
+
startCallAsOfferer(p.handle).catch(function(){ hangup(); });
|
|
1785
|
+
});
|
|
1786
|
+
actions.appendChild(chatBtn);
|
|
1787
|
+
actions.appendChild(btn);
|
|
1788
|
+
row.appendChild(who);
|
|
1789
|
+
row.appendChild(actions);
|
|
1790
|
+
liveRows.appendChild(row);
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1156
1794
|
async function refreshPeers(){
|
|
1157
1795
|
try {
|
|
1158
1796
|
var res = await fetch("/api/live/peers");
|
|
@@ -1160,26 +1798,15 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
1160
1798
|
var data = await res.json();
|
|
1161
1799
|
var peers = (data && data.peers) || [];
|
|
1162
1800
|
knownPeers = peers;
|
|
1801
|
+
peers.forEach(function(p){ ensureChatLoop(p.handle); });
|
|
1802
|
+
if (chatWith) {
|
|
1803
|
+
chatSub.textContent = peerKnown(chatWith)
|
|
1804
|
+
? "live over the P2P link \xB7 text only"
|
|
1805
|
+
: "peer offline \xB7 messages will not deliver";
|
|
1806
|
+
}
|
|
1163
1807
|
if (peers.length === 0) { livePanel.classList.remove("is-open"); return; }
|
|
1164
1808
|
livePanel.classList.add("is-open");
|
|
1165
|
-
|
|
1166
|
-
peers.forEach(function(p){
|
|
1167
|
-
var row = document.createElement("div");
|
|
1168
|
-
row.className = "live-row";
|
|
1169
|
-
var who = document.createElement("div");
|
|
1170
|
-
var h = document.createElement("div"); h.className = "h"; h.textContent = p.handle;
|
|
1171
|
-
var s = document.createElement("div"); s.className = "s"; s.textContent = p.league + " \xB7 " + p.harness;
|
|
1172
|
-
who.appendChild(h); who.appendChild(s);
|
|
1173
|
-
var btn = document.createElement("button");
|
|
1174
|
-
btn.className = "vbtn"; btn.type = "button"; btn.textContent = "Video";
|
|
1175
|
-
btn.addEventListener("click", function(){
|
|
1176
|
-
if (rtc.remoteHandle) return; // already in a call
|
|
1177
|
-
btn.disabled = true;
|
|
1178
|
-
startCallAsOfferer(p.handle).catch(function(){ btn.disabled = false; });
|
|
1179
|
-
});
|
|
1180
|
-
row.appendChild(who); row.appendChild(btn);
|
|
1181
|
-
liveRows.appendChild(row);
|
|
1182
|
-
});
|
|
1809
|
+
renderLiveRows(peers);
|
|
1183
1810
|
} catch(e){}
|
|
1184
1811
|
}
|
|
1185
1812
|
refreshPeers();
|
|
@@ -1190,19 +1817,37 @@ var webAppHtml = `<!DOCTYPE html>
|
|
|
1190
1817
|
</html>`;
|
|
1191
1818
|
|
|
1192
1819
|
// src/server.ts
|
|
1820
|
+
var MAX_QUEUED_MESSAGES = 200;
|
|
1193
1821
|
function createLiveBridge() {
|
|
1194
1822
|
const boxes = /* @__PURE__ */ new Map();
|
|
1195
1823
|
const bridge = {
|
|
1196
1824
|
get peers() {
|
|
1197
|
-
return [...boxes.values()].map((m) =>
|
|
1825
|
+
return [...boxes.values()].map((m) => {
|
|
1826
|
+
const h = m.link.hello;
|
|
1827
|
+
return {
|
|
1828
|
+
handle: h.handle,
|
|
1829
|
+
league: h.league,
|
|
1830
|
+
harness: h.harness,
|
|
1831
|
+
...h.verified !== void 0 ? { verified: h.verified } : {},
|
|
1832
|
+
...h.identityVerified !== void 0 ? { identityVerified: h.identityVerified } : {}
|
|
1833
|
+
};
|
|
1834
|
+
});
|
|
1198
1835
|
},
|
|
1199
1836
|
addLink(link) {
|
|
1200
1837
|
const handle2 = link.hello.handle;
|
|
1201
|
-
boxes.set(handle2, { link, incoming: [] });
|
|
1838
|
+
boxes.set(handle2, { link, incoming: [], messages: [] });
|
|
1202
1839
|
link.onSignal((f) => {
|
|
1203
1840
|
const mb = boxes.get(handle2);
|
|
1204
1841
|
if (mb) mb.incoming.push(f);
|
|
1205
1842
|
});
|
|
1843
|
+
link.onMessage((m) => {
|
|
1844
|
+
const mb = boxes.get(handle2);
|
|
1845
|
+
if (!mb) return;
|
|
1846
|
+
mb.messages.push({ ...m, text: sanitizePeerText(m.text) });
|
|
1847
|
+
if (mb.messages.length > MAX_QUEUED_MESSAGES) {
|
|
1848
|
+
mb.messages.splice(0, mb.messages.length - MAX_QUEUED_MESSAGES);
|
|
1849
|
+
}
|
|
1850
|
+
});
|
|
1206
1851
|
link.onClose(() => {
|
|
1207
1852
|
const cur = boxes.get(handle2);
|
|
1208
1853
|
if (cur && cur.link === link) boxes.delete(handle2);
|
|
@@ -1223,6 +1868,22 @@ function createLiveBridge() {
|
|
|
1223
1868
|
if (cur.incoming.length > 0) return cur.incoming.shift() ?? null;
|
|
1224
1869
|
}
|
|
1225
1870
|
return null;
|
|
1871
|
+
},
|
|
1872
|
+
sendMessage(handle2, text) {
|
|
1873
|
+
boxes.get(handle2)?.link.send(text);
|
|
1874
|
+
},
|
|
1875
|
+
async pollMessage(handle2, timeoutMs) {
|
|
1876
|
+
const mb = boxes.get(handle2);
|
|
1877
|
+
if (!mb) return null;
|
|
1878
|
+
if (mb.messages.length > 0) return mb.messages.shift() ?? null;
|
|
1879
|
+
const deadline = Date.now() + timeoutMs;
|
|
1880
|
+
while (Date.now() < deadline) {
|
|
1881
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1882
|
+
const cur = boxes.get(handle2);
|
|
1883
|
+
if (!cur) return null;
|
|
1884
|
+
if (cur.messages.length > 0) return cur.messages.shift() ?? null;
|
|
1885
|
+
}
|
|
1886
|
+
return null;
|
|
1226
1887
|
}
|
|
1227
1888
|
};
|
|
1228
1889
|
return bridge;
|
|
@@ -1396,11 +2057,62 @@ async function handle(req, res, opts) {
|
|
|
1396
2057
|
sendJson(res, 200, { ok: true });
|
|
1397
2058
|
return;
|
|
1398
2059
|
}
|
|
2060
|
+
if (req.method === "GET" && pathname === "/live/message") {
|
|
2061
|
+
const live = opts.live;
|
|
2062
|
+
if (!live) {
|
|
2063
|
+
sendJson(res, 200, { message: null, reason: "live-not-attached" });
|
|
2064
|
+
return;
|
|
2065
|
+
}
|
|
2066
|
+
const handle2 = url.searchParams.get("handle") ?? "";
|
|
2067
|
+
if (handle2 === "") {
|
|
2068
|
+
sendJson(res, 400, { error: "missing handle" });
|
|
2069
|
+
return;
|
|
2070
|
+
}
|
|
2071
|
+
const message = await live.pollMessage(handle2, 25e3);
|
|
2072
|
+
if (req.destroyed || res.writableEnded) return;
|
|
2073
|
+
sendJson(res, 200, { message });
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2076
|
+
if (req.method === "POST" && pathname === "/live/message") {
|
|
2077
|
+
const live = opts.live;
|
|
2078
|
+
if (!live) {
|
|
2079
|
+
sendJson(res, 400, { error: "live-not-attached" });
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
2082
|
+
const body = await readBody(req);
|
|
2083
|
+
let parsed = {};
|
|
2084
|
+
try {
|
|
2085
|
+
parsed = JSON.parse(body);
|
|
2086
|
+
} catch {
|
|
2087
|
+
sendJson(res, 400, { error: "invalid JSON body" });
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
const handle2 = typeof parsed["handle"] === "string" ? parsed["handle"] : "";
|
|
2091
|
+
if (handle2 === "") {
|
|
2092
|
+
sendJson(res, 400, { error: "missing handle" });
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
const text = parsed["text"];
|
|
2096
|
+
if (typeof text !== "string") {
|
|
2097
|
+
sendJson(res, 400, { error: "missing text" });
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
const reParsed = parseFrame(
|
|
2101
|
+
JSON.stringify({ t: "msg", id: randomUUID(), text, at: Date.now() })
|
|
2102
|
+
);
|
|
2103
|
+
if (reParsed === null || reParsed.t !== "msg") {
|
|
2104
|
+
sendJson(res, 400, { error: "invalid message text" });
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
2107
|
+
live.sendMessage(handle2, reParsed.text);
|
|
2108
|
+
sendJson(res, 200, { ok: true });
|
|
2109
|
+
return;
|
|
2110
|
+
}
|
|
1399
2111
|
sendJson(res, 404, { error: "not found" });
|
|
1400
2112
|
}
|
|
1401
2113
|
|
|
1402
2114
|
// src/cli.ts
|
|
1403
|
-
var VERSION = "0.
|
|
2115
|
+
var VERSION = "0.5.0";
|
|
1404
2116
|
function parsePort(raw) {
|
|
1405
2117
|
const n = Number(raw);
|
|
1406
2118
|
if (!Number.isInteger(n) || n < 1 || n > 65535) return void 0;
|
|
@@ -1414,7 +2126,8 @@ function parseArgs(argv) {
|
|
|
1414
2126
|
dating: false,
|
|
1415
2127
|
any: false,
|
|
1416
2128
|
arg: void 0,
|
|
1417
|
-
to: void 0
|
|
2129
|
+
to: void 0,
|
|
2130
|
+
keepAlive: false
|
|
1418
2131
|
};
|
|
1419
2132
|
for (let i = 0; i < argv.length; i++) {
|
|
1420
2133
|
const a = argv[i];
|
|
@@ -1426,7 +2139,8 @@ function parseArgs(argv) {
|
|
|
1426
2139
|
dating: false,
|
|
1427
2140
|
any: false,
|
|
1428
2141
|
arg: void 0,
|
|
1429
|
-
to: void 0
|
|
2142
|
+
to: void 0,
|
|
2143
|
+
keepAlive: false
|
|
1430
2144
|
};
|
|
1431
2145
|
}
|
|
1432
2146
|
if (a === "--help" || a === "-h") {
|
|
@@ -1437,13 +2151,18 @@ function parseArgs(argv) {
|
|
|
1437
2151
|
dating: false,
|
|
1438
2152
|
any: false,
|
|
1439
2153
|
arg: void 0,
|
|
1440
|
-
to: void 0
|
|
2154
|
+
to: void 0,
|
|
2155
|
+
keepAlive: false
|
|
1441
2156
|
};
|
|
1442
2157
|
}
|
|
1443
2158
|
if (a === "--live") {
|
|
1444
2159
|
out = { ...out, live: true };
|
|
1445
2160
|
continue;
|
|
1446
2161
|
}
|
|
2162
|
+
if (a === "--keep-alive") {
|
|
2163
|
+
out = { ...out, keepAlive: true };
|
|
2164
|
+
continue;
|
|
2165
|
+
}
|
|
1447
2166
|
if (a === "--any") {
|
|
1448
2167
|
out = { ...out, any: true };
|
|
1449
2168
|
continue;
|
|
@@ -1479,7 +2198,7 @@ function parseArgs(argv) {
|
|
|
1479
2198
|
continue;
|
|
1480
2199
|
}
|
|
1481
2200
|
if (a.startsWith("-")) continue;
|
|
1482
|
-
const known = a === "connect" || a === "matches" || a === "discover" || a === "open" || a === "live" || a === "find" || a === "handle" || a === "block" || a === "unblock" || a === "blocklist" || a === "mcp" || a === "help" ? a : null;
|
|
2201
|
+
const known = a === "connect" || a === "matches" || a === "discover" || a === "open" || a === "live" || a === "find" || a === "handle" || a === "block" || a === "unblock" || a === "blocklist" || a === "daemon" || a === "mcp" || a === "help" ? a : null;
|
|
1483
2202
|
if (known !== null && out.command === null) {
|
|
1484
2203
|
out = { ...out, command: known };
|
|
1485
2204
|
} else if (out.arg === void 0) {
|
|
@@ -1499,6 +2218,17 @@ function formatTokens(n) {
|
|
|
1499
2218
|
if (abs >= 1e3) return `${trim(n / 1e3)}k`;
|
|
1500
2219
|
return String(n);
|
|
1501
2220
|
}
|
|
2221
|
+
function formatAgo(iso, now = /* @__PURE__ */ new Date()) {
|
|
2222
|
+
const t = Date.parse(iso);
|
|
2223
|
+
if (Number.isNaN(t)) return "unknown";
|
|
2224
|
+
const s = Math.max(0, Math.floor((now.getTime() - t) / 1e3));
|
|
2225
|
+
if (s < 60) return "just now";
|
|
2226
|
+
const m = Math.floor(s / 60);
|
|
2227
|
+
if (m < 60) return `${m}m ago`;
|
|
2228
|
+
const h = Math.floor(m / 60);
|
|
2229
|
+
if (h < 24) return `${h}h ago`;
|
|
2230
|
+
return `${Math.floor(h / 24)}d ago`;
|
|
2231
|
+
}
|
|
1502
2232
|
function verificationText(snapshot) {
|
|
1503
2233
|
if (snapshot.source === "real") {
|
|
1504
2234
|
return `verified: real usage \u2014 ${formatTokens(snapshot.totalTokens)} tokens from ${snapshot.harness} logs`;
|
|
@@ -1545,7 +2275,9 @@ function idMark(peer) {
|
|
|
1545
2275
|
var MARKS_LEGEND = "marks: \u2713 usage verified (real local logs) \xB7 ~ unverified (self-report/demo/legacy) \xB7 \u{1F511} identity-verified (signed hello)";
|
|
1546
2276
|
async function cmdConnect() {
|
|
1547
2277
|
const harness = process2.env["VIBEDATING_HARNESS"] ?? "claude-code";
|
|
1548
|
-
const
|
|
2278
|
+
const ensured = ensureHandle();
|
|
2279
|
+
const handle2 = ensured.handle;
|
|
2280
|
+
const firstConnect = loadProfile() === null;
|
|
1549
2281
|
const snapshot = await readUsage(harness);
|
|
1550
2282
|
const profile = connectProfile(snapshot, handle2);
|
|
1551
2283
|
const identity = loadOrCreateIdentity();
|
|
@@ -1555,12 +2287,20 @@ async function cmdConnect() {
|
|
|
1555
2287
|
`);
|
|
1556
2288
|
process2.stdout.write(` handle: ${profile.handle} \xB7 harness: ${profile.harness}
|
|
1557
2289
|
`);
|
|
2290
|
+
if (ensured.generated) {
|
|
2291
|
+
process2.stdout.write(` assigned handle: ${profile.handle} \u2014 change it with: vibedate handle @name
|
|
2292
|
+
`);
|
|
2293
|
+
}
|
|
1558
2294
|
process2.stdout.write(` verification: ${verificationText(snapshot)}
|
|
1559
2295
|
`);
|
|
1560
2296
|
process2.stdout.write(` identity: ed25519 ${identity.publicKeyHex.slice(0, 12)}\u2026 \u2014 signs your hello (\u{1F511})
|
|
1561
2297
|
`);
|
|
1562
2298
|
process2.stdout.write("\n");
|
|
1563
|
-
process2.stdout.write(" \u2022 raw usage stays local \xB7 only league shared\n
|
|
2299
|
+
process2.stdout.write(" \u2022 raw usage stays local \xB7 only league shared\n");
|
|
2300
|
+
if (firstConnect) {
|
|
2301
|
+
process2.stdout.write(" \u2022 optional: get notified of new matches on login \u2014 `vibedate daemon install` (undo: daemon uninstall)\n");
|
|
2302
|
+
}
|
|
2303
|
+
process2.stdout.write("\n");
|
|
1564
2304
|
return 0;
|
|
1565
2305
|
}
|
|
1566
2306
|
async function cmdHandle(arg) {
|
|
@@ -1613,7 +2353,8 @@ async function cmdMatches(live) {
|
|
|
1613
2353
|
|
|
1614
2354
|
`);
|
|
1615
2355
|
for (const p of livePeers) {
|
|
1616
|
-
|
|
2356
|
+
const lastMsg = p.lastMessageAt !== void 0 ? ` \xB7 last msg ${formatAgo(p.lastMessageAt)}` : "";
|
|
2357
|
+
process2.stdout.write(` ${p.handle.padEnd(28)} ${p.league} \xB7 ${p.harness} ${usageMark(p)}${idMark(p)}${lastMsg}
|
|
1617
2358
|
`);
|
|
1618
2359
|
}
|
|
1619
2360
|
process2.stdout.write("\n");
|
|
@@ -1698,7 +2439,7 @@ async function cmdDiscover(live, any) {
|
|
|
1698
2439
|
);
|
|
1699
2440
|
return 0;
|
|
1700
2441
|
}
|
|
1701
|
-
async function cmdOpen(port) {
|
|
2442
|
+
async function cmdOpen(port, any) {
|
|
1702
2443
|
const profile = loadProfile();
|
|
1703
2444
|
let live;
|
|
1704
2445
|
let session;
|
|
@@ -1712,7 +2453,14 @@ async function cmdOpen(port) {
|
|
|
1712
2453
|
${LIVE_NOTICE}
|
|
1713
2454
|
`);
|
|
1714
2455
|
const hello = buildHello(profile);
|
|
1715
|
-
|
|
2456
|
+
const { topics, acceptLeague } = discoveryScope(profile.league, any);
|
|
2457
|
+
void startDiscovery({
|
|
2458
|
+
hello,
|
|
2459
|
+
topics,
|
|
2460
|
+
acceptLeague,
|
|
2461
|
+
isBlocked: blockedChecker(),
|
|
2462
|
+
onLink: (link) => live.addLink(link)
|
|
2463
|
+
}).then((s) => {
|
|
1716
2464
|
session = s;
|
|
1717
2465
|
}).catch(() => {
|
|
1718
2466
|
});
|
|
@@ -1721,9 +2469,11 @@ async function cmdOpen(port) {
|
|
|
1721
2469
|
vibedating local web app \u2192 ${started.url}
|
|
1722
2470
|
`);
|
|
1723
2471
|
if (live) {
|
|
1724
|
-
process2.stdout.write(
|
|
2472
|
+
process2.stdout.write(
|
|
2473
|
+
any ? " \u2022 live video + chat available for connected peers (ANY league \u2014 --any)\n" : " \u2022 live video + chat available for connected peers (your league + adjacent; --any = everyone)\n"
|
|
2474
|
+
);
|
|
1725
2475
|
} else {
|
|
1726
|
-
process2.stdout.write(" \u2022 connect first (`vibedating connect`) to enable live video\n");
|
|
2476
|
+
process2.stdout.write(" \u2022 connect first (`vibedating connect`) to enable live video + chat\n");
|
|
1727
2477
|
}
|
|
1728
2478
|
process2.stdout.write(" \u2022 raw usage stays local \xB7 only league shared\n");
|
|
1729
2479
|
process2.stdout.write(" (Ctrl+C to stop)\n\n");
|
|
@@ -1736,7 +2486,10 @@ async function cmdOpen(port) {
|
|
|
1736
2486
|
await new Promise((resolve) => started.server.close(() => resolve()));
|
|
1737
2487
|
return 0;
|
|
1738
2488
|
}
|
|
1739
|
-
|
|
2489
|
+
function shouldKeepAlive(flag, stdinIsTTY) {
|
|
2490
|
+
return flag || stdinIsTTY !== true;
|
|
2491
|
+
}
|
|
2492
|
+
async function cmdLive(dating, any, to, keepAlive) {
|
|
1740
2493
|
const profile = loadProfile();
|
|
1741
2494
|
if (!profile) {
|
|
1742
2495
|
process2.stderr.write("Not connected yet. Run `vibedating connect` first.\n");
|
|
@@ -1771,11 +2524,11 @@ async function cmdLive(dating, any, to) {
|
|
|
1771
2524
|
}
|
|
1772
2525
|
const { qual } = peerDirection(profile.league, link.hello.league);
|
|
1773
2526
|
process2.stdout.write(
|
|
1774
|
-
` \xB7 matched ${link.hello.handle} (${link.hello.league}${qual} \xB7 ${link.hello.harness}) ${usageMark(link.hello)}${idMark(link.hello)}
|
|
2527
|
+
` \xB7 matched ${sanitizePeerText(link.hello.handle)} (${link.hello.league}${qual} \xB7 ${link.hello.harness}) ${usageMark(link.hello)}${idMark(link.hello)}
|
|
1775
2528
|
`
|
|
1776
2529
|
);
|
|
1777
2530
|
link.onMessage((m) => {
|
|
1778
|
-
process2.stdout.write(` <${link.hello.handle}> ${m.text}
|
|
2531
|
+
process2.stdout.write(` <${sanitizePeerText(link.hello.handle)}> ${sanitizePeerText(m.text)}
|
|
1779
2532
|
`);
|
|
1780
2533
|
});
|
|
1781
2534
|
});
|
|
@@ -1789,14 +2542,14 @@ async function cmdLive(dating, any, to) {
|
|
|
1789
2542
|
if (target !== null && !sameHandle(link.hello.handle, target)) {
|
|
1790
2543
|
const { qual } = peerDirection(profile.league, link.hello.league);
|
|
1791
2544
|
process2.stdout.write(
|
|
1792
|
-
` + ${link.hello.handle} (${link.hello.league}${qual}) ${usageMark(link.hello)}${idMark(link.hello)} \u2014 not your target
|
|
2545
|
+
` + ${sanitizePeerText(link.hello.handle)} (${link.hello.league}${qual}) ${usageMark(link.hello)}${idMark(link.hello)} \u2014 not your target
|
|
1793
2546
|
`
|
|
1794
2547
|
);
|
|
1795
2548
|
link.close();
|
|
1796
2549
|
return;
|
|
1797
2550
|
}
|
|
1798
2551
|
if (target !== null) {
|
|
1799
|
-
process2.stdout.write(` \u2605 found ${link.hello.handle} \u2014 auto-opening
|
|
2552
|
+
process2.stdout.write(` \u2605 found ${sanitizePeerText(link.hello.handle)} \u2014 auto-opening
|
|
1800
2553
|
`);
|
|
1801
2554
|
}
|
|
1802
2555
|
pairing.add(link);
|
|
@@ -1814,9 +2567,13 @@ async function cmdLive(dating, any, to) {
|
|
|
1814
2567
|
};
|
|
1815
2568
|
process2.once("SIGINT", stop);
|
|
1816
2569
|
process2.once("SIGTERM", stop);
|
|
2570
|
+
let quitRequested = false;
|
|
1817
2571
|
for await (const line of rl) {
|
|
1818
2572
|
const text = line.trim();
|
|
1819
|
-
if (text === "/quit")
|
|
2573
|
+
if (text === "/quit") {
|
|
2574
|
+
quitRequested = true;
|
|
2575
|
+
break;
|
|
2576
|
+
}
|
|
1820
2577
|
if (text === "/next") {
|
|
1821
2578
|
pairing.next();
|
|
1822
2579
|
continue;
|
|
@@ -1837,6 +2594,13 @@ async function cmdLive(dating, any, to) {
|
|
|
1837
2594
|
process2.stdout.write(" \xB7 no peer yet \u2014 waiting for a match\u2026\n");
|
|
1838
2595
|
}
|
|
1839
2596
|
}
|
|
2597
|
+
if (!quitRequested && shouldKeepAlive(keepAlive, process2.stdin.isTTY)) {
|
|
2598
|
+
process2.stdout.write(" stdin closed \u2014 staying in the swarm (Ctrl+C / SIGTERM to leave)\n");
|
|
2599
|
+
await new Promise((resolve) => {
|
|
2600
|
+
process2.once("SIGINT", () => resolve());
|
|
2601
|
+
process2.once("SIGTERM", () => resolve());
|
|
2602
|
+
});
|
|
2603
|
+
}
|
|
1840
2604
|
process2.removeListener("SIGINT", stop);
|
|
1841
2605
|
process2.removeListener("SIGTERM", stop);
|
|
1842
2606
|
const cur = pairing.current();
|
|
@@ -1974,20 +2738,131 @@ async function cmdBlocklist() {
|
|
|
1974
2738
|
`);
|
|
1975
2739
|
return 0;
|
|
1976
2740
|
}
|
|
2741
|
+
async function cmdDaemonRun(any) {
|
|
2742
|
+
const profile = loadProfile();
|
|
2743
|
+
if (!profile) {
|
|
2744
|
+
process2.stderr.write("Not connected yet. Run `vibedating connect` first.\n");
|
|
2745
|
+
return 1;
|
|
2746
|
+
}
|
|
2747
|
+
if (!canShareLive()) grantLiveConsent();
|
|
2748
|
+
const dir = defaultStateDir();
|
|
2749
|
+
writeDaemonState(dir, { pid: process2.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString(), any, version: VERSION });
|
|
2750
|
+
const hello = buildHello(profile);
|
|
2751
|
+
const { topics, acceptLeague } = discoveryScope(profile.league, any);
|
|
2752
|
+
const session = await startDiscovery({
|
|
2753
|
+
hello,
|
|
2754
|
+
topics,
|
|
2755
|
+
acceptLeague,
|
|
2756
|
+
isBlocked: blockedChecker(),
|
|
2757
|
+
onPeer: (peer, isNew) => {
|
|
2758
|
+
process2.stdout.write(
|
|
2759
|
+
` [${(/* @__PURE__ */ new Date()).toISOString()}] ${isNew ? "NEW match" : "peer seen"}: ${sanitizePeerText(peer.handle)} (${peer.league} \xB7 ${peer.harness})
|
|
2760
|
+
`
|
|
2761
|
+
);
|
|
2762
|
+
}
|
|
2763
|
+
});
|
|
2764
|
+
process2.stdout.write(
|
|
2765
|
+
` vibedating daemon running (pid ${process2.pid}) \u2014 notify-only${any ? " \xB7 --any" : ""}
|
|
2766
|
+
`
|
|
2767
|
+
);
|
|
2768
|
+
await new Promise((resolve) => {
|
|
2769
|
+
process2.once("SIGINT", () => resolve());
|
|
2770
|
+
process2.once("SIGTERM", () => resolve());
|
|
2771
|
+
});
|
|
2772
|
+
await session.close();
|
|
2773
|
+
removeDaemonState(dir);
|
|
2774
|
+
process2.stdout.write(" daemon stopped\n");
|
|
2775
|
+
return 0;
|
|
2776
|
+
}
|
|
2777
|
+
async function cmdDaemon(arg, any) {
|
|
2778
|
+
switch (arg) {
|
|
2779
|
+
case "start": {
|
|
2780
|
+
if (loadProfile() === null) {
|
|
2781
|
+
process2.stderr.write("Not connected yet. Run `vibedating connect` first.\n");
|
|
2782
|
+
return 1;
|
|
2783
|
+
}
|
|
2784
|
+
const r = startDaemon({ any, version: VERSION });
|
|
2785
|
+
if (!r.started) {
|
|
2786
|
+
process2.stdout.write(` ${r.reason}
|
|
2787
|
+
`);
|
|
2788
|
+
return 0;
|
|
2789
|
+
}
|
|
2790
|
+
process2.stdout.write(` daemon started (pid ${r.pid}) \u2014 notify-only: alerts on NEW matches, never opens chat/video
|
|
2791
|
+
`);
|
|
2792
|
+
process2.stdout.write(` logs: ~/.vibedating/daemon.log \xB7 stop: vibedate daemon stop
|
|
2793
|
+
`);
|
|
2794
|
+
return 0;
|
|
2795
|
+
}
|
|
2796
|
+
case "stop": {
|
|
2797
|
+
const r = await stopDaemon();
|
|
2798
|
+
process2.stdout.write(
|
|
2799
|
+
r.stopped ? ` daemon stopped (pid ${r.pid})
|
|
2800
|
+
` : ` ${r.reason}
|
|
2801
|
+
`
|
|
2802
|
+
);
|
|
2803
|
+
return 0;
|
|
2804
|
+
}
|
|
2805
|
+
case "status": {
|
|
2806
|
+
const s = daemonStatus();
|
|
2807
|
+
if (s.running && s.state !== null) {
|
|
2808
|
+
process2.stdout.write(
|
|
2809
|
+
` daemon running (pid ${s.state.pid}) since ${s.state.startedAt}${s.state.any ? " \xB7 --any" : ""}
|
|
2810
|
+
`
|
|
2811
|
+
);
|
|
2812
|
+
} else {
|
|
2813
|
+
process2.stdout.write(" daemon not running\n");
|
|
2814
|
+
}
|
|
2815
|
+
return 0;
|
|
2816
|
+
}
|
|
2817
|
+
case "run":
|
|
2818
|
+
return cmdDaemonRun(any);
|
|
2819
|
+
case "install": {
|
|
2820
|
+
if (loadProfile() === null) {
|
|
2821
|
+
process2.stderr.write("Not connected yet. Run `vibedating connect` first.\n");
|
|
2822
|
+
return 1;
|
|
2823
|
+
}
|
|
2824
|
+
const r = installDaemonService({ any });
|
|
2825
|
+
process2.stdout.write(` ${r.detail}
|
|
2826
|
+
`);
|
|
2827
|
+
if (r.servicePath !== null) process2.stdout.write(` service: ${r.servicePath}
|
|
2828
|
+
`);
|
|
2829
|
+
if (r.installed) process2.stdout.write(" undo anytime: vibedate daemon uninstall\n");
|
|
2830
|
+
return r.installed ? 0 : 1;
|
|
2831
|
+
}
|
|
2832
|
+
case "uninstall": {
|
|
2833
|
+
const r = uninstallDaemonService({});
|
|
2834
|
+
process2.stdout.write(` ${r.detail}
|
|
2835
|
+
`);
|
|
2836
|
+
return 0;
|
|
2837
|
+
}
|
|
2838
|
+
default:
|
|
2839
|
+
process2.stderr.write("usage: vibedating daemon [start|stop|status|install|uninstall] [--any]\n");
|
|
2840
|
+
return 1;
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
1977
2843
|
var HELP = `vibedating ${VERSION} \u2014 dating by tokens (local-first)
|
|
1978
2844
|
|
|
1979
2845
|
Usage:
|
|
1980
2846
|
vibedating connect Read your usage, compute + print your league
|
|
1981
2847
|
vibedating matches [--live] List candidates in your league (live peers if any)
|
|
1982
2848
|
vibedating discover [--live] [--any] Find live peers over the DHT (your league + adjacent; --any = everyone)
|
|
1983
|
-
vibedating live [--dating] [--any] [--to @handle] Live chat (your league
|
|
2849
|
+
vibedating live [--dating] [--any] [--to @handle] [--keep-alive] Live chat (your league
|
|
2850
|
+
+ adjacent; --any = everyone; /next or --dating pick;
|
|
2851
|
+
--to targets one peer; --keep-alive survives stdin EOF \u2014
|
|
2852
|
+
automatic when stdin is not a TTY)
|
|
1984
2853
|
vibedating find <@handle> [--any] Search the DHT for one specific handle (\u2605 highlights a match)
|
|
1985
2854
|
vibedating handle [@name] Print or set your handle (persisted; a leading '@' is optional)
|
|
1986
2855
|
vibedating block <@handle> Block a handle \u2014 their hello is dropped (never recorded/paired)
|
|
1987
2856
|
vibedating unblock <@handle> Remove a handle from the blocklist
|
|
1988
2857
|
vibedating blocklist List blocked handles
|
|
1989
|
-
vibedating
|
|
1990
|
-
|
|
2858
|
+
vibedating daemon [start|stop|status|install|uninstall] [--any]
|
|
2859
|
+
Manage the notify-only background daemon \u2014 alerts on
|
|
2860
|
+
NEW matches, never opens chat/video. install adds a
|
|
2861
|
+
login service (launchd on macOS, systemd on Linux);
|
|
2862
|
+
uninstall removes it.
|
|
2863
|
+
vibedating open [--port N] [--any] Serve the local web app (default: random port)
|
|
2864
|
+
+ live video + chat with connected peers
|
|
2865
|
+
(your league + adjacent; --any = every league)
|
|
1991
2866
|
vibedating mcp Run the stdio MCP server (profile, matches)
|
|
1992
2867
|
vibedating --version
|
|
1993
2868
|
vibedating --help
|
|
@@ -2001,9 +2876,10 @@ Privacy:
|
|
|
2001
2876
|
(real local logs) \xB7 ~ unverified \xB7 \u{1F511} identity-verified (signed hello).
|
|
2002
2877
|
|
|
2003
2878
|
Matching:
|
|
2004
|
-
discover/live match your league + adjacent (\xB11) tiers by default, so
|
|
2005
|
-
leagues and cross-league friends still connect. --any matches every
|
|
2006
|
-
find / live --to look for one specific handle instead of the first
|
|
2879
|
+
discover/live/open match your league + adjacent (\xB11) tiers by default, so
|
|
2880
|
+
thin leagues and cross-league friends still connect. --any matches every
|
|
2881
|
+
league. find / live --to look for one specific handle instead of the first
|
|
2882
|
+
random peer.
|
|
2007
2883
|
|
|
2008
2884
|
Env:
|
|
2009
2885
|
VIBEDATING_TOKENS=<n> Self-report a token count (e.g. 23400000 or 12M)
|
|
@@ -2031,14 +2907,16 @@ async function main(argv) {
|
|
|
2031
2907
|
return cmdUnblock(parsed.arg);
|
|
2032
2908
|
case "blocklist":
|
|
2033
2909
|
return cmdBlocklist();
|
|
2910
|
+
case "daemon":
|
|
2911
|
+
return cmdDaemon(parsed.arg, parsed.any);
|
|
2034
2912
|
case "matches":
|
|
2035
2913
|
return cmdMatches(parsed.live);
|
|
2036
2914
|
case "discover":
|
|
2037
2915
|
return cmdDiscover(parsed.live, parsed.any);
|
|
2038
2916
|
case "open":
|
|
2039
|
-
return cmdOpen(parsed.port);
|
|
2917
|
+
return cmdOpen(parsed.port, parsed.any);
|
|
2040
2918
|
case "live":
|
|
2041
|
-
return cmdLive(parsed.dating, parsed.any, parsed.to);
|
|
2919
|
+
return cmdLive(parsed.dating, parsed.any, parsed.to, parsed.keepAlive);
|
|
2042
2920
|
case "find":
|
|
2043
2921
|
return cmdFind(parsed.arg, parsed.any);
|
|
2044
2922
|
case "mcp":
|
|
@@ -2069,5 +2947,7 @@ if (entryUrl !== void 0) {
|
|
|
2069
2947
|
}
|
|
2070
2948
|
}
|
|
2071
2949
|
export {
|
|
2072
|
-
|
|
2950
|
+
formatAgo,
|
|
2951
|
+
parseArgs,
|
|
2952
|
+
shouldKeepAlive
|
|
2073
2953
|
};
|