vibedate 0.4.1 → 0.6.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/dist/cli.js CHANGED
@@ -1,15 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runMcp
4
- } from "./chunk-ZB56ZBAR.js";
4
+ } from "./chunk-JIBC3OHV.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
+ createNostrPoolTransport,
16
+ createNostrRelayLink,
17
+ defaultStateDir,
13
18
  grantLiveConsent,
14
19
  isBlocked,
15
20
  league,
@@ -17,7 +22,9 @@ import {
17
22
  leagueTopic,
18
23
  leaguesWithin,
19
24
  loadBlocklist,
25
+ loadHandle,
20
26
  loadOrCreateIdentity,
27
+ loadOrCreateNostrKey,
21
28
  loadPeers,
22
29
  loadProfile,
23
30
  matches,
@@ -27,16 +34,356 @@ import {
27
34
  removeBlock,
28
35
  resolveHandle,
29
36
  sameHandle,
37
+ sanitizePeerText,
30
38
  saveHandle,
31
39
  signHelloClaims,
32
40
  startDiscovery
33
- } from "./chunk-43IYNWES.js";
41
+ } from "./chunk-VZXPYU2C.js";
34
42
 
35
43
  // src/cli.ts
36
44
  import readline from "readline";
37
45
  import "url";
38
46
  import process2 from "process";
39
47
 
48
+ // src/handlegen.ts
49
+ import { randomBytes } from "crypto";
50
+ var FIRST = [
51
+ "segfault",
52
+ "yak",
53
+ "vibe",
54
+ "null",
55
+ "async",
56
+ "await",
57
+ "heap",
58
+ "stack",
59
+ "sudo",
60
+ "regex",
61
+ "token",
62
+ "prompt",
63
+ "context",
64
+ "merge",
65
+ "rebase",
66
+ "hotfix",
67
+ "flaky",
68
+ "cursed",
69
+ "based",
70
+ "quantum",
71
+ "turbo",
72
+ "neural",
73
+ "agentic",
74
+ "kernel",
75
+ "docker",
76
+ "kube",
77
+ "lambda",
78
+ "pointer",
79
+ "buffer",
80
+ "packet",
81
+ "syscall",
82
+ "runtime",
83
+ "monad",
84
+ "borrow",
85
+ "cache",
86
+ "deadlock",
87
+ "localhost",
88
+ "darkmode",
89
+ "wasm",
90
+ "diff"
91
+ ];
92
+ var SECOND = [
93
+ "sommelier",
94
+ "shaver",
95
+ "goblin",
96
+ "nomad",
97
+ "gremlin",
98
+ "wizard",
99
+ "bard",
100
+ "pirate",
101
+ "ninja",
102
+ "monk",
103
+ "prophet",
104
+ "oracle",
105
+ "smith",
106
+ "farmer",
107
+ "whisperer",
108
+ "tamer",
109
+ "connoisseur",
110
+ "maximalist",
111
+ "enjoyer",
112
+ "dealer",
113
+ "herder",
114
+ "wrangler",
115
+ "juggler",
116
+ "mechanic",
117
+ "surgeon",
118
+ "detective",
119
+ "librarian",
120
+ "alchemist",
121
+ "overlord",
122
+ "apprentice",
123
+ "sensei",
124
+ "chef",
125
+ "dj",
126
+ "ranger",
127
+ "paladin",
128
+ "barbarian",
129
+ "summoner",
130
+ "cartographer",
131
+ "archivist",
132
+ "plumber"
133
+ ];
134
+ var SUFFIX = [
135
+ "prime",
136
+ "9000",
137
+ "3000",
138
+ "max",
139
+ "ultra",
140
+ "xl",
141
+ "mk2",
142
+ "777",
143
+ "404",
144
+ "1337",
145
+ "2077",
146
+ "xd"
147
+ ];
148
+ function cryptoRand() {
149
+ return randomBytes(4).readUInt32BE(0) / 2 ** 32;
150
+ }
151
+ function pick(list, rand) {
152
+ return list[Math.min(list.length - 1, Math.floor(rand() * list.length))];
153
+ }
154
+ function generateHandle(rand = cryptoRand) {
155
+ const first = pick(FIRST, rand);
156
+ const second = pick(SECOND, rand);
157
+ let body = `${first}_${second}`;
158
+ if (rand() < 0.5) {
159
+ const withSuffix = `${body}_${pick(SUFFIX, rand)}`;
160
+ if (withSuffix.length + 1 <= MAX_HANDLE_LEN) body = withSuffix;
161
+ }
162
+ const canonical = normalizeHandle(body);
163
+ if (canonical === null) throw new Error(`handle generator produced an invalid handle: ${body}`);
164
+ return canonical;
165
+ }
166
+ function ensureHandle(dir = defaultStateDir()) {
167
+ const env = process.env["VIBEDATING_HANDLE"];
168
+ if (env !== void 0 && env.trim() !== "") {
169
+ const canonical = normalizeHandle(env);
170
+ if (canonical !== null) return { handle: canonical, generated: false };
171
+ }
172
+ const persisted = loadHandle(dir);
173
+ if (persisted !== DEFAULT_HANDLE) return { handle: persisted, generated: false };
174
+ const generated = generateHandle();
175
+ return { handle: saveHandle(generated, dir), generated: true };
176
+ }
177
+
178
+ // src/daemon.ts
179
+ import { spawn, spawnSync } from "child_process";
180
+ import { mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from "fs";
181
+ import os from "os";
182
+ import path from "path";
183
+ function daemonStatePath(dir) {
184
+ return path.join(dir, "daemon.json");
185
+ }
186
+ function daemonLogPath(dir) {
187
+ return path.join(dir, "daemon.log");
188
+ }
189
+ function readDaemonState(dir = defaultStateDir()) {
190
+ try {
191
+ const raw = readFileSync(daemonStatePath(dir), "utf8");
192
+ const data = JSON.parse(raw);
193
+ if (typeof data["pid"] !== "number" || !Number.isInteger(data["pid"]) || data["pid"] <= 0 || typeof data["startedAt"] !== "string" || typeof data["any"] !== "boolean" || typeof data["version"] !== "string") {
194
+ return null;
195
+ }
196
+ return { pid: data["pid"], startedAt: data["startedAt"], any: data["any"], version: data["version"] };
197
+ } catch {
198
+ return null;
199
+ }
200
+ }
201
+ function writeDaemonState(dir, state) {
202
+ mkdirSync(dir, { recursive: true });
203
+ writeFileSync(daemonStatePath(dir), JSON.stringify(state, null, 2) + "\n", "utf8");
204
+ }
205
+ function removeDaemonState(dir = defaultStateDir()) {
206
+ try {
207
+ rmSync(daemonStatePath(dir), { force: true });
208
+ } catch {
209
+ }
210
+ }
211
+ function isPidAlive(pid, kill = process.kill) {
212
+ try {
213
+ kill(pid, 0);
214
+ return true;
215
+ } catch (err) {
216
+ return err.code === "EPERM";
217
+ }
218
+ }
219
+ function daemonStatus(dir = defaultStateDir(), alive = isPidAlive) {
220
+ const state = readDaemonState(dir);
221
+ if (state === null) return { running: false, state: null };
222
+ if (alive(state.pid)) return { running: true, state };
223
+ removeDaemonState(dir);
224
+ return { running: false, state: null };
225
+ }
226
+ function defaultSpawn(execPath, scriptPath, args, logPath) {
227
+ mkdirSync(path.dirname(logPath), { recursive: true });
228
+ const fd = openSync(logPath, "a");
229
+ const child = spawn(execPath, [scriptPath, ...args], {
230
+ detached: true,
231
+ stdio: ["ignore", fd, fd]
232
+ });
233
+ child.unref();
234
+ if (child.pid === void 0) throw new Error("could not spawn daemon child process");
235
+ return child.pid;
236
+ }
237
+ function startDaemon(opts) {
238
+ const dir = opts.dir ?? defaultStateDir();
239
+ const status = daemonStatus(dir, opts.alive);
240
+ if (status.running && status.state !== null) {
241
+ return { started: false, reason: `already running (pid ${status.state.pid})` };
242
+ }
243
+ const execPath = opts.execPath ?? process.execPath;
244
+ const scriptPath = opts.scriptPath ?? process.argv[1];
245
+ if (scriptPath === void 0) return { started: false, reason: "cannot locate the CLI entry point" };
246
+ const args = ["daemon", "run", ...opts.any ? ["--any"] : []];
247
+ const spawnProcess = opts.spawnProcess ?? ((a, logPath) => defaultSpawn(execPath, scriptPath, [...a], logPath));
248
+ const pid = spawnProcess(args, daemonLogPath(dir));
249
+ writeDaemonState(dir, { pid, startedAt: (/* @__PURE__ */ new Date()).toISOString(), any: opts.any, version: opts.version });
250
+ return { started: true, pid };
251
+ }
252
+ async function stopDaemon(opts = {}) {
253
+ const dir = opts.dir ?? defaultStateDir();
254
+ const kill = opts.kill ?? process.kill;
255
+ const alive = opts.alive ?? ((pid) => isPidAlive(pid, kill));
256
+ const state = readDaemonState(dir);
257
+ if (state === null) return { stopped: false, reason: "not running" };
258
+ if (!alive(state.pid)) {
259
+ removeDaemonState(dir);
260
+ return { stopped: false, reason: "not running (cleaned stale pidfile)" };
261
+ }
262
+ try {
263
+ kill(state.pid, "SIGTERM");
264
+ } catch {
265
+ }
266
+ const deadline = Date.now() + (opts.waitMs ?? 2e3);
267
+ while (Date.now() < deadline && alive(state.pid)) {
268
+ await new Promise((r) => setTimeout(r, 50));
269
+ }
270
+ removeDaemonState(dir);
271
+ return { stopped: true, pid: state.pid };
272
+ }
273
+ var DAEMON_SERVICE_LABEL = "ai.vibedating.daemon";
274
+ var SYSTEMD_UNIT_NAME = "vibedating.service";
275
+ function daemonServicePath(platform = process.platform, homeDir = os.homedir()) {
276
+ if (platform === "darwin") {
277
+ return path.join(homeDir, "Library", "LaunchAgents", `${DAEMON_SERVICE_LABEL}.plist`);
278
+ }
279
+ if (platform === "linux") {
280
+ return path.join(homeDir, ".config", "systemd", "user", SYSTEMD_UNIT_NAME);
281
+ }
282
+ return null;
283
+ }
284
+ function serviceArgv(execPath, scriptPath, any) {
285
+ return [execPath, scriptPath, "daemon", "run", ...any ? ["--any"] : []];
286
+ }
287
+ function renderLaunchdPlist(opts) {
288
+ const args = serviceArgv(opts.execPath, opts.scriptPath, opts.any).map((a) => ` <string>${a}</string>`).join("\n");
289
+ return `<?xml version="1.0" encoding="UTF-8"?>
290
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
291
+ <plist version="1.0">
292
+ <dict>
293
+ <key>Label</key>
294
+ <string>${DAEMON_SERVICE_LABEL}</string>
295
+ <key>ProgramArguments</key>
296
+ <array>
297
+ ${args}
298
+ </array>
299
+ <key>RunAtLoad</key>
300
+ <true/>
301
+ <key>KeepAlive</key>
302
+ <false/>
303
+ <key>StandardOutPath</key>
304
+ <string>${opts.logPath}</string>
305
+ <key>StandardErrorPath</key>
306
+ <string>${opts.logPath}</string>
307
+ </dict>
308
+ </plist>
309
+ `;
310
+ }
311
+ function renderSystemdUnit(opts) {
312
+ const execStart = serviceArgv(opts.execPath, opts.scriptPath, opts.any).join(" ");
313
+ return `[Unit]
314
+ Description=vibedating notify-only daemon (alerts on new matches; never opens chat/video)
315
+
316
+ [Service]
317
+ ExecStart=${execStart}
318
+ Restart=on-failure
319
+ RestartSec=10
320
+
321
+ [Install]
322
+ WantedBy=default.target
323
+ `;
324
+ }
325
+ var defaultRun = (cmd, args) => spawnSync(cmd, args, { stdio: "inherit" }).status === 0;
326
+ function installDaemonService(opts) {
327
+ const platform = opts.platform ?? process.platform;
328
+ const homeDir = opts.homeDir ?? os.homedir();
329
+ const run = opts.run ?? defaultRun;
330
+ const servicePath = daemonServicePath(platform, homeDir);
331
+ if (servicePath === null) {
332
+ return {
333
+ installed: false,
334
+ servicePath: null,
335
+ detail: `unsupported platform (${platform}) \u2014 run \`vibedate daemon start\` manually instead`
336
+ };
337
+ }
338
+ const execPath = opts.execPath ?? process.execPath;
339
+ const scriptPath = opts.scriptPath ?? process.argv[1];
340
+ if (scriptPath === void 0) {
341
+ return { installed: false, servicePath, detail: "cannot locate the CLI entry point" };
342
+ }
343
+ mkdirSync(path.dirname(servicePath), { recursive: true });
344
+ if (platform === "darwin") {
345
+ const logPath = daemonLogPath(opts.dir ?? defaultStateDir());
346
+ writeFileSync(servicePath, renderLaunchdPlist({ execPath, scriptPath, any: opts.any, logPath }), "utf8");
347
+ const uid = process.getuid?.() ?? 501;
348
+ run("launchctl", ["bootout", `gui/${uid}`, servicePath]);
349
+ if (!run("launchctl", ["bootstrap", `gui/${uid}`, servicePath])) {
350
+ return { installed: false, servicePath, detail: "launchctl bootstrap failed \u2014 service written but not loaded" };
351
+ }
352
+ return { installed: true, servicePath, detail: "launchd agent installed \u2014 the daemon starts on login" };
353
+ }
354
+ writeFileSync(servicePath, renderSystemdUnit({ execPath, scriptPath, any: opts.any }), "utf8");
355
+ run("systemctl", ["--user", "daemon-reload"]);
356
+ if (!run("systemctl", ["--user", "enable", "--now", SYSTEMD_UNIT_NAME])) {
357
+ return { installed: false, servicePath, detail: "systemctl enable failed \u2014 unit written but not started" };
358
+ }
359
+ return { installed: true, servicePath, detail: "systemd --user service installed \u2014 the daemon starts on login" };
360
+ }
361
+ function uninstallDaemonService(opts) {
362
+ const platform = opts.platform ?? process.platform;
363
+ const homeDir = opts.homeDir ?? os.homedir();
364
+ const run = opts.run ?? defaultRun;
365
+ const servicePath = daemonServicePath(platform, homeDir);
366
+ if (servicePath === null) {
367
+ return {
368
+ installed: false,
369
+ servicePath: null,
370
+ detail: `unsupported platform (${platform})`
371
+ };
372
+ }
373
+ if (platform === "darwin") {
374
+ const uid = process.getuid?.() ?? 501;
375
+ run("launchctl", ["bootout", `gui/${uid}`, servicePath]);
376
+ } else {
377
+ run("systemctl", ["--user", "disable", "--now", SYSTEMD_UNIT_NAME]);
378
+ run("systemctl", ["--user", "daemon-reload"]);
379
+ }
380
+ try {
381
+ rmSync(servicePath, { force: true });
382
+ } catch {
383
+ }
384
+ return { installed: false, servicePath, detail: "login service removed \u2014 the daemon no longer starts on login" };
385
+ }
386
+
40
387
  // src/pairing.ts
41
388
  function createPairing() {
42
389
  const queue = [];
@@ -1499,7 +1846,7 @@ function createLiveBridge() {
1499
1846
  link.onMessage((m) => {
1500
1847
  const mb = boxes.get(handle2);
1501
1848
  if (!mb) return;
1502
- mb.messages.push(m);
1849
+ mb.messages.push({ ...m, text: sanitizePeerText(m.text) });
1503
1850
  if (mb.messages.length > MAX_QUEUED_MESSAGES) {
1504
1851
  mb.messages.splice(0, mb.messages.length - MAX_QUEUED_MESSAGES);
1505
1852
  }
@@ -1768,7 +2115,7 @@ async function handle(req, res, opts) {
1768
2115
  }
1769
2116
 
1770
2117
  // src/cli.ts
1771
- var VERSION = "0.4.1";
2118
+ var VERSION = "0.6.0";
1772
2119
  function parsePort(raw) {
1773
2120
  const n = Number(raw);
1774
2121
  if (!Number.isInteger(n) || n < 1 || n > 65535) return void 0;
@@ -1782,7 +2129,9 @@ function parseArgs(argv) {
1782
2129
  dating: false,
1783
2130
  any: false,
1784
2131
  arg: void 0,
1785
- to: void 0
2132
+ to: void 0,
2133
+ keepAlive: false,
2134
+ viaRelay: false
1786
2135
  };
1787
2136
  for (let i = 0; i < argv.length; i++) {
1788
2137
  const a = argv[i];
@@ -1794,7 +2143,9 @@ function parseArgs(argv) {
1794
2143
  dating: false,
1795
2144
  any: false,
1796
2145
  arg: void 0,
1797
- to: void 0
2146
+ to: void 0,
2147
+ keepAlive: false,
2148
+ viaRelay: false
1798
2149
  };
1799
2150
  }
1800
2151
  if (a === "--help" || a === "-h") {
@@ -1805,13 +2156,23 @@ function parseArgs(argv) {
1805
2156
  dating: false,
1806
2157
  any: false,
1807
2158
  arg: void 0,
1808
- to: void 0
2159
+ to: void 0,
2160
+ keepAlive: false,
2161
+ viaRelay: false
1809
2162
  };
1810
2163
  }
1811
2164
  if (a === "--live") {
1812
2165
  out = { ...out, live: true };
1813
2166
  continue;
1814
2167
  }
2168
+ if (a === "--keep-alive") {
2169
+ out = { ...out, keepAlive: true };
2170
+ continue;
2171
+ }
2172
+ if (a === "--via-relay") {
2173
+ out = { ...out, viaRelay: true };
2174
+ continue;
2175
+ }
1815
2176
  if (a === "--any") {
1816
2177
  out = { ...out, any: true };
1817
2178
  continue;
@@ -1847,7 +2208,7 @@ function parseArgs(argv) {
1847
2208
  continue;
1848
2209
  }
1849
2210
  if (a.startsWith("-")) continue;
1850
- 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;
2211
+ 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;
1851
2212
  if (known !== null && out.command === null) {
1852
2213
  out = { ...out, command: known };
1853
2214
  } else if (out.arg === void 0) {
@@ -1867,6 +2228,17 @@ function formatTokens(n) {
1867
2228
  if (abs >= 1e3) return `${trim(n / 1e3)}k`;
1868
2229
  return String(n);
1869
2230
  }
2231
+ function formatAgo(iso, now = /* @__PURE__ */ new Date()) {
2232
+ const t = Date.parse(iso);
2233
+ if (Number.isNaN(t)) return "unknown";
2234
+ const s = Math.max(0, Math.floor((now.getTime() - t) / 1e3));
2235
+ if (s < 60) return "just now";
2236
+ const m = Math.floor(s / 60);
2237
+ if (m < 60) return `${m}m ago`;
2238
+ const h = Math.floor(m / 60);
2239
+ if (h < 24) return `${h}h ago`;
2240
+ return `${Math.floor(h / 24)}d ago`;
2241
+ }
1870
2242
  function verificationText(snapshot) {
1871
2243
  if (snapshot.source === "real") {
1872
2244
  return `verified: real usage \u2014 ${formatTokens(snapshot.totalTokens)} tokens from ${snapshot.harness} logs`;
@@ -1913,7 +2285,9 @@ function idMark(peer) {
1913
2285
  var MARKS_LEGEND = "marks: \u2713 usage verified (real local logs) \xB7 ~ unverified (self-report/demo/legacy) \xB7 \u{1F511} identity-verified (signed hello)";
1914
2286
  async function cmdConnect() {
1915
2287
  const harness = process2.env["VIBEDATING_HARNESS"] ?? "claude-code";
1916
- const handle2 = resolveHandle();
2288
+ const ensured = ensureHandle();
2289
+ const handle2 = ensured.handle;
2290
+ const firstConnect = loadProfile() === null;
1917
2291
  const snapshot = await readUsage(harness);
1918
2292
  const profile = connectProfile(snapshot, handle2);
1919
2293
  const identity = loadOrCreateIdentity();
@@ -1923,12 +2297,20 @@ async function cmdConnect() {
1923
2297
  `);
1924
2298
  process2.stdout.write(` handle: ${profile.handle} \xB7 harness: ${profile.harness}
1925
2299
  `);
2300
+ if (ensured.generated) {
2301
+ process2.stdout.write(` assigned handle: ${profile.handle} \u2014 change it with: vibedate handle @name
2302
+ `);
2303
+ }
1926
2304
  process2.stdout.write(` verification: ${verificationText(snapshot)}
1927
2305
  `);
1928
2306
  process2.stdout.write(` identity: ed25519 ${identity.publicKeyHex.slice(0, 12)}\u2026 \u2014 signs your hello (\u{1F511})
1929
2307
  `);
1930
2308
  process2.stdout.write("\n");
1931
- process2.stdout.write(" \u2022 raw usage stays local \xB7 only league shared\n\n");
2309
+ process2.stdout.write(" \u2022 raw usage stays local \xB7 only league shared\n");
2310
+ if (firstConnect) {
2311
+ process2.stdout.write(" \u2022 optional: get notified of new matches on login \u2014 `vibedate daemon install` (undo: daemon uninstall)\n");
2312
+ }
2313
+ process2.stdout.write("\n");
1932
2314
  return 0;
1933
2315
  }
1934
2316
  async function cmdHandle(arg) {
@@ -1981,7 +2363,8 @@ async function cmdMatches(live) {
1981
2363
 
1982
2364
  `);
1983
2365
  for (const p of livePeers) {
1984
- process2.stdout.write(` ${p.handle.padEnd(28)} ${p.league} \xB7 ${p.harness} ${usageMark(p)}${idMark(p)}
2366
+ const lastMsg = p.lastMessageAt !== void 0 ? ` \xB7 last msg ${formatAgo(p.lastMessageAt)}` : "";
2367
+ process2.stdout.write(` ${p.handle.padEnd(28)} ${p.league} \xB7 ${p.harness} ${usageMark(p)}${idMark(p)}${lastMsg}
1985
2368
  `);
1986
2369
  }
1987
2370
  process2.stdout.write("\n");
@@ -2008,7 +2391,7 @@ async function cmdMatches(live) {
2008
2391
  process2.stdout.write("\n");
2009
2392
  return 0;
2010
2393
  }
2011
- async function cmdDiscover(live, any) {
2394
+ async function cmdDiscover(live, any, viaRelay) {
2012
2395
  const profile = loadProfile();
2013
2396
  if (!profile) {
2014
2397
  process2.stderr.write("Not connected yet. Run `vibedating connect` first.\n");
@@ -2027,6 +2410,9 @@ async function cmdDiscover(live, any) {
2027
2410
  `);
2028
2411
  process2.stdout.write(` ${MARKS_LEGEND}
2029
2412
  `);
2413
+ if (viaRelay) {
2414
+ process2.stdout.write(" \u2022 --via-relay: the relay fallback reaches a KNOWN peer over Nostr \u2014 use `live --via-relay --to @handle`\n");
2415
+ }
2030
2416
  const { topics, acceptLeague } = discoveryScope(profile.league, any);
2031
2417
  const session = await startDiscovery({
2032
2418
  hello,
@@ -2113,7 +2499,103 @@ async function cmdOpen(port, any) {
2113
2499
  await new Promise((resolve) => started.server.close(() => resolve()));
2114
2500
  return 0;
2115
2501
  }
2116
- async function cmdLive(dating, any, to) {
2502
+ function shouldKeepAlive(flag, stdinIsTTY) {
2503
+ return flag || stdinIsTTY !== true;
2504
+ }
2505
+ async function cmdLiveViaRelay(profile, to) {
2506
+ const target = to !== void 0 ? normalizeHandle(to) : null;
2507
+ if (to !== void 0 && target === null) {
2508
+ process2.stderr.write(`invalid target handle: ${to}
2509
+ `);
2510
+ return 1;
2511
+ }
2512
+ if (target === null) {
2513
+ process2.stderr.write(
2514
+ "relay mode (v0) targets one known peer \u2014 use: vibedating live --via-relay --to <@handle>\n(discover the peer first with `vibedating discover --live`, then reach them over the relay).\n"
2515
+ );
2516
+ return 1;
2517
+ }
2518
+ const peer = loadPeers().find((p) => sameHandle(p.handle, target) && typeof p.pubkey === "string");
2519
+ if (peer === void 0 || peer.pubkey === void 0) {
2520
+ process2.stderr.write(
2521
+ `${target} is not a known peer with an identity pubkey. Run \`vibedating discover --live\` first to meet them over the DHT, then retry --via-relay.
2522
+ `
2523
+ );
2524
+ return 1;
2525
+ }
2526
+ const identity = loadOrCreateIdentity();
2527
+ process2.stdout.write("\n");
2528
+ process2.stdout.write(` ${LIVE_NOTICE}
2529
+ `);
2530
+ process2.stdout.write(
2531
+ ` routing e2e chat to ${sanitizePeerText(peer.handle)} through public Nostr relays (direct hole-punch fallback)
2532
+ `
2533
+ );
2534
+ process2.stdout.write(" \u2022 the relay stores only ciphertext \u2014 it cannot read your chat\n");
2535
+ const myNostr = await loadOrCreateNostrKey();
2536
+ const transport = await createNostrPoolTransport();
2537
+ const link = await createNostrRelayLink({
2538
+ myNostr,
2539
+ myEd25519Hex: identity.publicKeyHex,
2540
+ peerEd25519Hex: peer.pubkey,
2541
+ hello: peer,
2542
+ transport
2543
+ });
2544
+ const pairing = createPairing();
2545
+ pairing.onMatch((l) => {
2546
+ if (l === void 0) {
2547
+ process2.stdout.write(" \xB7 relay peer gone\n");
2548
+ return;
2549
+ }
2550
+ const { qual } = peerDirection(profile.league, l.hello.league);
2551
+ process2.stdout.write(
2552
+ ` \xB7 relayed to ${sanitizePeerText(l.hello.handle)} (${l.hello.league}${qual} \xB7 ${l.hello.harness}) ${usageMark(l.hello)}${idMark(l.hello)}
2553
+ `
2554
+ );
2555
+ l.onMessage((m) => {
2556
+ process2.stdout.write(` <${sanitizePeerText(l.hello.handle)}> ${sanitizePeerText(m.text)}
2557
+ `);
2558
+ });
2559
+ });
2560
+ pairing.add(link);
2561
+ process2.stdout.write(" type to chat \xB7 /quit\n");
2562
+ process2.stdout.write(" (Ctrl+C to stop)\n\n");
2563
+ const rl = readline.createInterface({ input: process2.stdin, terminal: false });
2564
+ const stop = () => {
2565
+ rl.close();
2566
+ };
2567
+ process2.once("SIGINT", stop);
2568
+ process2.once("SIGTERM", stop);
2569
+ let quitRequested = false;
2570
+ for await (const line of rl) {
2571
+ const text = line.trim();
2572
+ if (text === "/quit") {
2573
+ quitRequested = true;
2574
+ break;
2575
+ }
2576
+ if (text === "/next") {
2577
+ process2.stdout.write(" \xB7 relay mode targets one peer \u2014 /next re-announces presence\n");
2578
+ continue;
2579
+ }
2580
+ if (text === "") continue;
2581
+ const cur2 = pairing.current();
2582
+ if (cur2 !== void 0) {
2583
+ cur2.send(text);
2584
+ } else {
2585
+ process2.stdout.write(" \xB7 no relay peer yet \u2014 waiting for the presence exchange\u2026\n");
2586
+ }
2587
+ }
2588
+ process2.removeListener("SIGINT", stop);
2589
+ process2.removeListener("SIGTERM", stop);
2590
+ void quitRequested;
2591
+ const cur = pairing.current();
2592
+ if (cur !== void 0) cur.close();
2593
+ process2.stdout.write("\n closing relay link\u2026\n");
2594
+ link.close();
2595
+ process2.stdout.write("\n");
2596
+ return 0;
2597
+ }
2598
+ async function cmdLive(dating, any, to, keepAlive, viaRelay) {
2117
2599
  const profile = loadProfile();
2118
2600
  if (!profile) {
2119
2601
  process2.stderr.write("Not connected yet. Run `vibedating connect` first.\n");
@@ -2130,6 +2612,7 @@ async function cmdLive(dating, any, to) {
2130
2612
  process2.stderr.write("Could not enable live discovery. Try `vibedating discover --live`.\n");
2131
2613
  return 1;
2132
2614
  }
2615
+ if (viaRelay) return cmdLiveViaRelay(profile, to);
2133
2616
  const hello = buildHello(profile);
2134
2617
  process2.stdout.write("\n");
2135
2618
  process2.stdout.write(` ${LIVE_NOTICE}
@@ -2148,11 +2631,11 @@ async function cmdLive(dating, any, to) {
2148
2631
  }
2149
2632
  const { qual } = peerDirection(profile.league, link.hello.league);
2150
2633
  process2.stdout.write(
2151
- ` \xB7 matched ${link.hello.handle} (${link.hello.league}${qual} \xB7 ${link.hello.harness}) ${usageMark(link.hello)}${idMark(link.hello)}
2634
+ ` \xB7 matched ${sanitizePeerText(link.hello.handle)} (${link.hello.league}${qual} \xB7 ${link.hello.harness}) ${usageMark(link.hello)}${idMark(link.hello)}
2152
2635
  `
2153
2636
  );
2154
2637
  link.onMessage((m) => {
2155
- process2.stdout.write(` <${link.hello.handle}> ${m.text}
2638
+ process2.stdout.write(` <${sanitizePeerText(link.hello.handle)}> ${sanitizePeerText(m.text)}
2156
2639
  `);
2157
2640
  });
2158
2641
  });
@@ -2166,14 +2649,14 @@ async function cmdLive(dating, any, to) {
2166
2649
  if (target !== null && !sameHandle(link.hello.handle, target)) {
2167
2650
  const { qual } = peerDirection(profile.league, link.hello.league);
2168
2651
  process2.stdout.write(
2169
- ` + ${link.hello.handle} (${link.hello.league}${qual}) ${usageMark(link.hello)}${idMark(link.hello)} \u2014 not your target
2652
+ ` + ${sanitizePeerText(link.hello.handle)} (${link.hello.league}${qual}) ${usageMark(link.hello)}${idMark(link.hello)} \u2014 not your target
2170
2653
  `
2171
2654
  );
2172
2655
  link.close();
2173
2656
  return;
2174
2657
  }
2175
2658
  if (target !== null) {
2176
- process2.stdout.write(` \u2605 found ${link.hello.handle} \u2014 auto-opening
2659
+ process2.stdout.write(` \u2605 found ${sanitizePeerText(link.hello.handle)} \u2014 auto-opening
2177
2660
  `);
2178
2661
  }
2179
2662
  pairing.add(link);
@@ -2191,9 +2674,13 @@ async function cmdLive(dating, any, to) {
2191
2674
  };
2192
2675
  process2.once("SIGINT", stop);
2193
2676
  process2.once("SIGTERM", stop);
2677
+ let quitRequested = false;
2194
2678
  for await (const line of rl) {
2195
2679
  const text = line.trim();
2196
- if (text === "/quit") break;
2680
+ if (text === "/quit") {
2681
+ quitRequested = true;
2682
+ break;
2683
+ }
2197
2684
  if (text === "/next") {
2198
2685
  pairing.next();
2199
2686
  continue;
@@ -2214,6 +2701,13 @@ async function cmdLive(dating, any, to) {
2214
2701
  process2.stdout.write(" \xB7 no peer yet \u2014 waiting for a match\u2026\n");
2215
2702
  }
2216
2703
  }
2704
+ if (!quitRequested && shouldKeepAlive(keepAlive, process2.stdin.isTTY)) {
2705
+ process2.stdout.write(" stdin closed \u2014 staying in the swarm (Ctrl+C / SIGTERM to leave)\n");
2706
+ await new Promise((resolve) => {
2707
+ process2.once("SIGINT", () => resolve());
2708
+ process2.once("SIGTERM", () => resolve());
2709
+ });
2710
+ }
2217
2711
  process2.removeListener("SIGINT", stop);
2218
2712
  process2.removeListener("SIGTERM", stop);
2219
2713
  const cur = pairing.current();
@@ -2351,18 +2845,129 @@ async function cmdBlocklist() {
2351
2845
  `);
2352
2846
  return 0;
2353
2847
  }
2848
+ async function cmdDaemonRun(any) {
2849
+ const profile = loadProfile();
2850
+ if (!profile) {
2851
+ process2.stderr.write("Not connected yet. Run `vibedating connect` first.\n");
2852
+ return 1;
2853
+ }
2854
+ if (!canShareLive()) grantLiveConsent();
2855
+ const dir = defaultStateDir();
2856
+ writeDaemonState(dir, { pid: process2.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString(), any, version: VERSION });
2857
+ const hello = buildHello(profile);
2858
+ const { topics, acceptLeague } = discoveryScope(profile.league, any);
2859
+ const session = await startDiscovery({
2860
+ hello,
2861
+ topics,
2862
+ acceptLeague,
2863
+ isBlocked: blockedChecker(),
2864
+ onPeer: (peer, isNew) => {
2865
+ process2.stdout.write(
2866
+ ` [${(/* @__PURE__ */ new Date()).toISOString()}] ${isNew ? "NEW match" : "peer seen"}: ${sanitizePeerText(peer.handle)} (${peer.league} \xB7 ${peer.harness})
2867
+ `
2868
+ );
2869
+ }
2870
+ });
2871
+ process2.stdout.write(
2872
+ ` vibedating daemon running (pid ${process2.pid}) \u2014 notify-only${any ? " \xB7 --any" : ""}
2873
+ `
2874
+ );
2875
+ await new Promise((resolve) => {
2876
+ process2.once("SIGINT", () => resolve());
2877
+ process2.once("SIGTERM", () => resolve());
2878
+ });
2879
+ await session.close();
2880
+ removeDaemonState(dir);
2881
+ process2.stdout.write(" daemon stopped\n");
2882
+ return 0;
2883
+ }
2884
+ async function cmdDaemon(arg, any) {
2885
+ switch (arg) {
2886
+ case "start": {
2887
+ if (loadProfile() === null) {
2888
+ process2.stderr.write("Not connected yet. Run `vibedating connect` first.\n");
2889
+ return 1;
2890
+ }
2891
+ const r = startDaemon({ any, version: VERSION });
2892
+ if (!r.started) {
2893
+ process2.stdout.write(` ${r.reason}
2894
+ `);
2895
+ return 0;
2896
+ }
2897
+ process2.stdout.write(` daemon started (pid ${r.pid}) \u2014 notify-only: alerts on NEW matches, never opens chat/video
2898
+ `);
2899
+ process2.stdout.write(` logs: ~/.vibedating/daemon.log \xB7 stop: vibedate daemon stop
2900
+ `);
2901
+ return 0;
2902
+ }
2903
+ case "stop": {
2904
+ const r = await stopDaemon();
2905
+ process2.stdout.write(
2906
+ r.stopped ? ` daemon stopped (pid ${r.pid})
2907
+ ` : ` ${r.reason}
2908
+ `
2909
+ );
2910
+ return 0;
2911
+ }
2912
+ case "status": {
2913
+ const s = daemonStatus();
2914
+ if (s.running && s.state !== null) {
2915
+ process2.stdout.write(
2916
+ ` daemon running (pid ${s.state.pid}) since ${s.state.startedAt}${s.state.any ? " \xB7 --any" : ""}
2917
+ `
2918
+ );
2919
+ } else {
2920
+ process2.stdout.write(" daemon not running\n");
2921
+ }
2922
+ return 0;
2923
+ }
2924
+ case "run":
2925
+ return cmdDaemonRun(any);
2926
+ case "install": {
2927
+ if (loadProfile() === null) {
2928
+ process2.stderr.write("Not connected yet. Run `vibedating connect` first.\n");
2929
+ return 1;
2930
+ }
2931
+ const r = installDaemonService({ any });
2932
+ process2.stdout.write(` ${r.detail}
2933
+ `);
2934
+ if (r.servicePath !== null) process2.stdout.write(` service: ${r.servicePath}
2935
+ `);
2936
+ if (r.installed) process2.stdout.write(" undo anytime: vibedate daemon uninstall\n");
2937
+ return r.installed ? 0 : 1;
2938
+ }
2939
+ case "uninstall": {
2940
+ const r = uninstallDaemonService({});
2941
+ process2.stdout.write(` ${r.detail}
2942
+ `);
2943
+ return 0;
2944
+ }
2945
+ default:
2946
+ process2.stderr.write("usage: vibedating daemon [start|stop|status|install|uninstall] [--any]\n");
2947
+ return 1;
2948
+ }
2949
+ }
2354
2950
  var HELP = `vibedating ${VERSION} \u2014 dating by tokens (local-first)
2355
2951
 
2356
2952
  Usage:
2357
2953
  vibedating connect Read your usage, compute + print your league
2358
2954
  vibedating matches [--live] List candidates in your league (live peers if any)
2359
2955
  vibedating discover [--live] [--any] Find live peers over the DHT (your league + adjacent; --any = everyone)
2360
- vibedating live [--dating] [--any] [--to @handle] Live chat (your league + adjacent; --any = everyone; /next or --dating pick; --to targets one peer)
2956
+ vibedating live [--dating] [--any] [--to @handle] [--keep-alive] [--via-relay] Live chat (your league
2957
+ + adjacent; --any = everyone; /next or --dating pick;
2958
+ --to targets one peer; --keep-alive survives stdin EOF \u2014
2959
+ automatic when stdin is not a TTY; --via-relay routes e2e chat
2960
+ through public Nostr relays when direct hole-punching fails)
2361
2961
  vibedating find <@handle> [--any] Search the DHT for one specific handle (\u2605 highlights a match)
2362
2962
  vibedating handle [@name] Print or set your handle (persisted; a leading '@' is optional)
2363
2963
  vibedating block <@handle> Block a handle \u2014 their hello is dropped (never recorded/paired)
2364
2964
  vibedating unblock <@handle> Remove a handle from the blocklist
2365
2965
  vibedating blocklist List blocked handles
2966
+ vibedating daemon [start|stop|status|install|uninstall] [--any]
2967
+ Manage the notify-only background daemon \u2014 alerts on
2968
+ NEW matches, never opens chat/video. install adds a
2969
+ login service (launchd on macOS, systemd on Linux);
2970
+ uninstall removes it.
2366
2971
  vibedating open [--port N] [--any] Serve the local web app (default: random port)
2367
2972
  + live video + chat with connected peers
2368
2973
  (your league + adjacent; --any = every league)
@@ -2410,14 +3015,16 @@ async function main(argv) {
2410
3015
  return cmdUnblock(parsed.arg);
2411
3016
  case "blocklist":
2412
3017
  return cmdBlocklist();
3018
+ case "daemon":
3019
+ return cmdDaemon(parsed.arg, parsed.any);
2413
3020
  case "matches":
2414
3021
  return cmdMatches(parsed.live);
2415
3022
  case "discover":
2416
- return cmdDiscover(parsed.live, parsed.any);
3023
+ return cmdDiscover(parsed.live, parsed.any, parsed.viaRelay);
2417
3024
  case "open":
2418
3025
  return cmdOpen(parsed.port, parsed.any);
2419
3026
  case "live":
2420
- return cmdLive(parsed.dating, parsed.any, parsed.to);
3027
+ return cmdLive(parsed.dating, parsed.any, parsed.to, parsed.keepAlive, parsed.viaRelay);
2421
3028
  case "find":
2422
3029
  return cmdFind(parsed.arg, parsed.any);
2423
3030
  case "mcp":
@@ -2448,5 +3055,7 @@ if (entryUrl !== void 0) {
2448
3055
  }
2449
3056
  }
2450
3057
  export {
2451
- parseArgs
3058
+ formatAgo,
3059
+ parseArgs,
3060
+ shouldKeepAlive
2452
3061
  };