vibedate 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -432,6 +432,115 @@ function grantLiveConsent(dir = defaultStateDir()) {
432
432
  function canShareLive(dir = defaultStateDir()) {
433
433
  return createLedger(dir).allows(LIVE_CONSENT_SCOPE);
434
434
  }
435
+ var HANDLE_FILE = "handle.json";
436
+ var DEFAULT_HANDLE = "@you";
437
+ var MAX_HANDLE_LEN = 32;
438
+ function handleFilePath(dir) {
439
+ return path2.join(dir, HANDLE_FILE);
440
+ }
441
+ function stripLeadingAt(handle) {
442
+ return handle.replace(/^@+/, "");
443
+ }
444
+ function sameHandle(a, b) {
445
+ return stripLeadingAt(a) === stripLeadingAt(b);
446
+ }
447
+ function normalizeHandle(input) {
448
+ if (typeof input !== "string") return null;
449
+ const trimmed = input.trim();
450
+ if (trimmed.length === 0) return null;
451
+ const body = stripLeadingAt(trimmed);
452
+ if (body.length === 0) return null;
453
+ if (/\s/.test(body) || /[\x00-\x1f\x7f]/.test(body)) return null;
454
+ const canonical = "@" + body;
455
+ if (canonical.length > MAX_HANDLE_LEN) return null;
456
+ return canonical;
457
+ }
458
+ function loadHandle(dir = defaultStateDir()) {
459
+ try {
460
+ const raw = readFileSync(handleFilePath(dir), "utf8");
461
+ const data = JSON.parse(raw);
462
+ if (typeof data["handle"] !== "string") return DEFAULT_HANDLE;
463
+ return normalizeHandle(data["handle"]) ?? DEFAULT_HANDLE;
464
+ } catch {
465
+ return DEFAULT_HANDLE;
466
+ }
467
+ }
468
+ function saveHandle(handle, dir = defaultStateDir()) {
469
+ const canonical = normalizeHandle(handle);
470
+ if (canonical === null) {
471
+ throw new Error(
472
+ `invalid handle: use 1-${MAX_HANDLE_LEN} chars (optional leading '@'), no whitespace`
473
+ );
474
+ }
475
+ mkdirSync(dir, { recursive: true });
476
+ writeFileSync2(handleFilePath(dir), JSON.stringify({ handle: canonical }, null, 2) + "\n", "utf8");
477
+ const existing = loadProfile(dir);
478
+ if (existing !== null) {
479
+ writeFileSync2(
480
+ profilePath(dir),
481
+ JSON.stringify({ ...existing, handle: canonical }, null, 2) + "\n",
482
+ "utf8"
483
+ );
484
+ }
485
+ return canonical;
486
+ }
487
+ function resolveHandle(dir = defaultStateDir()) {
488
+ const env = process.env["VIBEDATING_HANDLE"];
489
+ if (env !== void 0 && env.trim() !== "") {
490
+ const canonical = normalizeHandle(env);
491
+ if (canonical !== null) return canonical;
492
+ }
493
+ return loadHandle(dir);
494
+ }
495
+ var BLOCKLIST_FILE = "blocklist.json";
496
+ function blocklistPath(dir) {
497
+ return path2.join(dir, BLOCKLIST_FILE);
498
+ }
499
+ function persistBlocklist(blocked, dir) {
500
+ mkdirSync(dir, { recursive: true });
501
+ writeFileSync2(blocklistPath(dir), JSON.stringify({ blocked }, null, 2) + "\n", "utf8");
502
+ }
503
+ function loadBlocklist(dir = defaultStateDir()) {
504
+ try {
505
+ const raw = readFileSync(blocklistPath(dir), "utf8");
506
+ const data = JSON.parse(raw);
507
+ if (!Array.isArray(data["blocked"])) return [];
508
+ return data["blocked"].filter((h) => typeof h === "string");
509
+ } catch {
510
+ return [];
511
+ }
512
+ }
513
+ function isBlocked(handle, dir = defaultStateDir()) {
514
+ const want = stripLeadingAt(handle);
515
+ if (want === "") return false;
516
+ return loadBlocklist(dir).some((entry) => stripLeadingAt(entry) === want);
517
+ }
518
+ function addBlock(handle, dir = defaultStateDir()) {
519
+ const canonical = normalizeHandle(handle);
520
+ if (canonical === null) {
521
+ throw new Error(
522
+ `invalid handle: use 1-${MAX_HANDLE_LEN} chars (optional leading '@'), no whitespace`
523
+ );
524
+ }
525
+ const list = loadBlocklist(dir);
526
+ if (list.some((e) => sameHandle(e, canonical))) return { blocked: list, changed: false };
527
+ const blocked = [...list, canonical];
528
+ persistBlocklist(blocked, dir);
529
+ return { blocked, changed: true };
530
+ }
531
+ function removeBlock(handle, dir = defaultStateDir()) {
532
+ const canonical = normalizeHandle(handle);
533
+ if (canonical === null) {
534
+ throw new Error(
535
+ `invalid handle: use 1-${MAX_HANDLE_LEN} chars (optional leading '@'), no whitespace`
536
+ );
537
+ }
538
+ const list = loadBlocklist(dir);
539
+ const blocked = list.filter((e) => !sameHandle(e, canonical));
540
+ if (blocked.length === list.length) return { blocked: list, changed: false };
541
+ persistBlocklist(blocked, dir);
542
+ return { blocked, changed: true };
543
+ }
435
544
 
436
545
  // src/p2p.ts
437
546
  var TOPIC_PREFIX = "vibedate:";
@@ -439,7 +548,7 @@ function leagueTopic(leagueName) {
439
548
  return createHash("sha256").update(`${TOPIC_PREFIX}${leagueName}`, "utf8").digest();
440
549
  }
441
550
  var LIVE_NOTICE = "live discovery: sharing only your handle + league + harness (never raw usage) with same-league peers on the public DHT";
442
- var MAX_HANDLE_LEN = 64;
551
+ var MAX_HANDLE_LEN2 = 64;
443
552
  var MAX_LEAGUE_LEN = 32;
444
553
  var MAX_HARNESS_LEN = 64;
445
554
  var MAX_HANDSHAKE_LEN = 4096;
@@ -464,7 +573,7 @@ function parseHandshake(raw) {
464
573
  const rec = data;
465
574
  const handle = rec["handle"];
466
575
  const league2 = rec["league"];
467
- if (typeof handle !== "string" || handle.length === 0 || handle.length > MAX_HANDLE_LEN) {
576
+ if (typeof handle !== "string" || handle.length === 0 || handle.length > MAX_HANDLE_LEN2) {
468
577
  return null;
469
578
  }
470
579
  if (typeof league2 !== "string" || league2.length === 0 || league2.length > MAX_LEAGUE_LEN) {
@@ -511,6 +620,7 @@ function recordPeer(hello, dir = defaultStateDir(), now = /* @__PURE__ */ new Da
511
620
  }
512
621
  async function startDiscovery(opts) {
513
622
  const { hello, stateDir = defaultStateDir(), onPeer, onLink, notify = vibeCoreNotify } = opts;
623
+ const isBlocked2 = opts.isBlocked;
514
624
  const topics = opts.topics ? [...opts.topics] : opts.topic !== void 0 ? [opts.topic] : [leagueTopic(hello.league)];
515
625
  const acceptLeague = opts.acceptLeague ?? ((l) => l === hello.league);
516
626
  const { default: Hyperswarm } = await import("hyperswarm");
@@ -541,6 +651,7 @@ async function startDiscovery(opts) {
541
651
  harness: frame.harness
542
652
  };
543
653
  if (!acceptLeague(peer.league)) continue;
654
+ if (isBlocked2 !== void 0 && isBlocked2(peer.handle)) continue;
544
655
  peers.set(remoteKey, peer);
545
656
  const { isNew } = recordPeer(peer, stateDir);
546
657
  if (isNew) {
@@ -745,6 +856,14 @@ export {
745
856
  loadProfile,
746
857
  grantLiveConsent,
747
858
  canShareLive,
859
+ sameHandle,
860
+ normalizeHandle,
861
+ saveHandle,
862
+ resolveHandle,
863
+ loadBlocklist,
864
+ isBlocked,
865
+ addBlock,
866
+ removeBlock,
748
867
  TOPIC_PREFIX,
749
868
  leagueTopic,
750
869
  LIVE_NOTICE,
@@ -2,7 +2,7 @@ import {
2
2
  CANDIDATES,
3
3
  loadProfile,
4
4
  matches
5
- } from "./chunk-VBLFAOV3.js";
5
+ } from "./chunk-3VT4EOBX.js";
6
6
 
7
7
  // src/mcp.ts
8
8
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
package/dist/cli.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /** Recognized top-level commands, plus the synthetic help/version. */
3
- type Command = 'connect' | 'matches' | 'discover' | 'open' | 'live' | 'mcp' | 'help' | 'version' | null;
3
+ type Command = 'connect' | 'matches' | 'discover' | 'open' | 'live' | 'find' | 'handle' | 'block' | 'unblock' | 'blocklist' | 'mcp' | 'help' | 'version' | null;
4
4
  interface ParsedArgs {
5
5
  readonly command: Command;
6
6
  /** Port for `open --port`; undefined means "let the OS pick". */
@@ -11,6 +11,10 @@ interface ParsedArgs {
11
11
  readonly dating: boolean;
12
12
  /** `discover --any` / `live --any`: match every league (not just ±1). Default false. */
13
13
  readonly any: boolean;
14
+ /** Positional argument for `handle`/`find` (e.g. `@name`). */
15
+ readonly arg: string | undefined;
16
+ /** `live --to <@handle>`: targeted match — auto-open that specific peer. */
17
+ readonly to: string | undefined;
14
18
  }
15
19
  /**
16
20
  * Parse argv (the slice AFTER the program name) into a command + options.
package/dist/cli.js CHANGED
@@ -1,26 +1,34 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runMcp
4
- } from "./chunk-DMA7MSN7.js";
4
+ } from "./chunk-OVO2CQ7X.js";
5
5
  import {
6
6
  CANDIDATES,
7
7
  LIVE_NOTICE,
8
8
  TOPIC_PREFIX,
9
+ addBlock,
9
10
  allLeagueNames,
10
11
  canShareLive,
11
12
  connectProfile,
12
13
  grantLiveConsent,
14
+ isBlocked,
13
15
  league,
14
16
  leagueIndex,
15
17
  leagueTopic,
16
18
  leaguesWithin,
19
+ loadBlocklist,
17
20
  loadPeers,
18
21
  loadProfile,
19
22
  matches,
23
+ normalizeHandle,
20
24
  parseFrame,
21
25
  readUsage,
26
+ removeBlock,
27
+ resolveHandle,
28
+ sameHandle,
29
+ saveHandle,
22
30
  startDiscovery
23
- } from "./chunk-VBLFAOV3.js";
31
+ } from "./chunk-3VT4EOBX.js";
24
32
 
25
33
  // src/cli.ts
26
34
  import readline from "readline";
@@ -1390,21 +1398,45 @@ async function handle(req, res, opts) {
1390
1398
  }
1391
1399
 
1392
1400
  // src/cli.ts
1393
- var VERSION = "0.3.0";
1401
+ var VERSION = "0.3.1";
1394
1402
  function parsePort(raw) {
1395
1403
  const n = Number(raw);
1396
1404
  if (!Number.isInteger(n) || n < 1 || n > 65535) return void 0;
1397
1405
  return n;
1398
1406
  }
1399
1407
  function parseArgs(argv) {
1400
- let out = { command: null, port: void 0, live: false, dating: false, any: false };
1408
+ let out = {
1409
+ command: null,
1410
+ port: void 0,
1411
+ live: false,
1412
+ dating: false,
1413
+ any: false,
1414
+ arg: void 0,
1415
+ to: void 0
1416
+ };
1401
1417
  for (let i = 0; i < argv.length; i++) {
1402
1418
  const a = argv[i];
1403
1419
  if (a === "--version" || a === "-v") {
1404
- return { command: "version", port: void 0, live: false, dating: false, any: false };
1420
+ return {
1421
+ command: "version",
1422
+ port: void 0,
1423
+ live: false,
1424
+ dating: false,
1425
+ any: false,
1426
+ arg: void 0,
1427
+ to: void 0
1428
+ };
1405
1429
  }
1406
1430
  if (a === "--help" || a === "-h") {
1407
- return { command: "help", port: void 0, live: false, dating: false, any: false };
1431
+ return {
1432
+ command: "help",
1433
+ port: void 0,
1434
+ live: false,
1435
+ dating: false,
1436
+ any: false,
1437
+ arg: void 0,
1438
+ to: void 0
1439
+ };
1408
1440
  }
1409
1441
  if (a === "--live") {
1410
1442
  out = { ...out, live: true };
@@ -1432,10 +1464,24 @@ function parseArgs(argv) {
1432
1464
  if (p !== void 0) out = { ...out, port: p };
1433
1465
  continue;
1434
1466
  }
1467
+ if (a === "--to") {
1468
+ const next = argv[i + 1];
1469
+ if (next !== void 0) {
1470
+ out = { ...out, to: next };
1471
+ i++;
1472
+ }
1473
+ continue;
1474
+ }
1475
+ if (a.startsWith("--to=")) {
1476
+ out = { ...out, to: a.slice("--to=".length) };
1477
+ continue;
1478
+ }
1435
1479
  if (a.startsWith("-")) continue;
1436
- const known = a === "connect" || a === "matches" || a === "discover" || a === "open" || a === "live" || a === "mcp" || a === "help" ? a : null;
1480
+ 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;
1437
1481
  if (known !== null && out.command === null) {
1438
1482
  out = { ...out, command: known };
1483
+ } else if (out.arg === void 0) {
1484
+ out = { ...out, arg: a };
1439
1485
  }
1440
1486
  }
1441
1487
  return out;
@@ -1455,6 +1501,9 @@ function discoveryScope(myLeague, any) {
1455
1501
  acceptLeague: (peerLeague) => accepted.has(peerLeague)
1456
1502
  };
1457
1503
  }
1504
+ function blockedChecker() {
1505
+ return (handle2) => isBlocked(handle2);
1506
+ }
1458
1507
  function peerDirection(myLeague, peerLeague, sameBullet = "+") {
1459
1508
  const d = leagueIndex(peerLeague) - leagueIndex(myLeague);
1460
1509
  if (d > 0) return { bullet: "\u2191", qual: " \xB7 higher league" };
@@ -1463,7 +1512,7 @@ function peerDirection(myLeague, peerLeague, sameBullet = "+") {
1463
1512
  }
1464
1513
  async function cmdConnect() {
1465
1514
  const harness = process2.env["VIBEDATING_HARNESS"] ?? "claude-code";
1466
- const handle2 = process2.env["VIBEDATING_HANDLE"] ?? "@you";
1515
+ const handle2 = resolveHandle();
1467
1516
  const snapshot = await readUsage(harness);
1468
1517
  const profile = connectProfile(snapshot, handle2);
1469
1518
  const lg = league(snapshot.totalTokens);
@@ -1480,6 +1529,28 @@ async function cmdConnect() {
1480
1529
  process2.stdout.write(" \u2022 raw usage stays local \xB7 only league shared\n\n");
1481
1530
  return 0;
1482
1531
  }
1532
+ async function cmdHandle(arg) {
1533
+ if (arg === void 0 || arg.trim() === "") {
1534
+ const handle2 = resolveHandle();
1535
+ process2.stdout.write(`${handle2}
1536
+ `);
1537
+ const env = process2.env["VIBEDATING_HANDLE"];
1538
+ if (env !== void 0 && env.trim() !== "" && normalizeHandle(env) !== null) {
1539
+ process2.stdout.write(" (env VIBEDATING_HANDLE overrides the persisted handle for this run)\n");
1540
+ }
1541
+ return 0;
1542
+ }
1543
+ try {
1544
+ const canonical = saveHandle(arg);
1545
+ process2.stdout.write(` handle set \u2192 ${canonical} (saved to ~/.vibedating/handle.json)
1546
+ `);
1547
+ return 0;
1548
+ } catch (err) {
1549
+ process2.stderr.write(`${err instanceof Error ? err.message : String(err)}
1550
+ `);
1551
+ return 1;
1552
+ }
1553
+ }
1483
1554
  async function cmdMatches(live) {
1484
1555
  const profile = loadProfile();
1485
1556
  if (!profile) {
@@ -1547,7 +1618,7 @@ async function cmdDiscover(live, any) {
1547
1618
  return 1;
1548
1619
  }
1549
1620
  const hello = {
1550
- handle: profile.handle,
1621
+ handle: resolveHandle(),
1551
1622
  league: profile.league,
1552
1623
  harness: profile.harness
1553
1624
  };
@@ -1559,6 +1630,7 @@ async function cmdDiscover(live, any) {
1559
1630
  hello,
1560
1631
  topics,
1561
1632
  acceptLeague,
1633
+ isBlocked: blockedChecker(),
1562
1634
  onPeer: (peer, isNew) => {
1563
1635
  const { bullet, qual } = peerDirection(profile.league, peer.league);
1564
1636
  process2.stdout.write(
@@ -1606,11 +1678,11 @@ async function cmdOpen(port) {
1606
1678
  ${LIVE_NOTICE}
1607
1679
  `);
1608
1680
  const hello = {
1609
- handle: profile.handle,
1681
+ handle: resolveHandle(),
1610
1682
  league: profile.league,
1611
1683
  harness: profile.harness
1612
1684
  };
1613
- void startDiscovery({ hello, onLink: (link) => live.addLink(link) }).then((s) => {
1685
+ void startDiscovery({ hello, isBlocked: blockedChecker(), onLink: (link) => live.addLink(link) }).then((s) => {
1614
1686
  session = s;
1615
1687
  }).catch(() => {
1616
1688
  });
@@ -1634,19 +1706,25 @@ async function cmdOpen(port) {
1634
1706
  await new Promise((resolve) => started.server.close(() => resolve()));
1635
1707
  return 0;
1636
1708
  }
1637
- async function cmdLive(dating, any) {
1709
+ async function cmdLive(dating, any, to) {
1638
1710
  const profile = loadProfile();
1639
1711
  if (!profile) {
1640
1712
  process2.stderr.write("Not connected yet. Run `vibedating connect` first.\n");
1641
1713
  return 1;
1642
1714
  }
1715
+ const target = to !== void 0 ? normalizeHandle(to) : null;
1716
+ if (to !== void 0 && target === null) {
1717
+ process2.stderr.write(`invalid target handle: ${to}
1718
+ `);
1719
+ return 1;
1720
+ }
1643
1721
  if (!canShareLive()) grantLiveConsent();
1644
1722
  if (!canShareLive()) {
1645
1723
  process2.stderr.write("Could not enable live discovery. Try `vibedating discover --live`.\n");
1646
1724
  return 1;
1647
1725
  }
1648
1726
  const hello = {
1649
- handle: profile.handle,
1727
+ handle: resolveHandle(),
1650
1728
  league: profile.league,
1651
1729
  harness: profile.harness
1652
1730
  };
@@ -1654,7 +1732,8 @@ async function cmdLive(dating, any) {
1654
1732
  process2.stdout.write(` ${LIVE_NOTICE}
1655
1733
  `);
1656
1734
  process2.stdout.write(
1657
- dating ? " dating mode \u2014 /open <handle> to pick a peer\n" : " omegle mode \u2014 /next to roll a new peer\n"
1735
+ target !== null ? ` targeted \u2014 auto-opening ${target} when they connect (/quit to stop)
1736
+ ` : dating ? " dating mode \u2014 /open <handle> to pick a peer\n" : " omegle mode \u2014 /next to roll a new peer\n"
1658
1737
  );
1659
1738
  const pairing = createPairing();
1660
1739
  pairing.onMatch((link) => {
@@ -1677,7 +1756,23 @@ async function cmdLive(dating, any) {
1677
1756
  hello,
1678
1757
  topics,
1679
1758
  acceptLeague,
1680
- onLink: (link) => pairing.add(link)
1759
+ isBlocked: blockedChecker(),
1760
+ onLink: (link) => {
1761
+ if (target !== null && !sameHandle(link.hello.handle, target)) {
1762
+ const { qual } = peerDirection(profile.league, link.hello.league);
1763
+ process2.stdout.write(
1764
+ ` + ${link.hello.handle} (${link.hello.league}${qual}) \u2014 not your target
1765
+ `
1766
+ );
1767
+ link.close();
1768
+ return;
1769
+ }
1770
+ if (target !== null) {
1771
+ process2.stdout.write(` \u2605 found ${link.hello.handle} \u2014 auto-opening
1772
+ `);
1773
+ }
1774
+ pairing.add(link);
1775
+ }
1681
1776
  });
1682
1777
  process2.stdout.write(
1683
1778
  ` topic: ${TOPIC_PREFIX}${profile.league} \u2192 ${session.topic.toString("hex").slice(0, 12)}\u2026${topics.length > 1 ? ` (+${topics.length - 1} more)` : ""}
@@ -1723,13 +1818,144 @@ async function cmdLive(dating, any) {
1723
1818
  process2.stdout.write("\n");
1724
1819
  return 0;
1725
1820
  }
1821
+ async function cmdFind(targetArg, any) {
1822
+ if (targetArg === void 0 || targetArg.trim() === "") {
1823
+ process2.stderr.write("usage: vibedating find <@handle> [--any]\n");
1824
+ return 1;
1825
+ }
1826
+ const target = normalizeHandle(targetArg);
1827
+ if (target === null) {
1828
+ process2.stderr.write(`invalid handle: ${targetArg}
1829
+ `);
1830
+ return 1;
1831
+ }
1832
+ const profile = loadProfile();
1833
+ if (!profile) {
1834
+ process2.stderr.write("Not connected yet. Run `vibedating connect` first.\n");
1835
+ return 1;
1836
+ }
1837
+ if (!canShareLive()) grantLiveConsent();
1838
+ if (!canShareLive()) {
1839
+ process2.stderr.write("Could not enable live discovery. Try `vibedating discover --live`.\n");
1840
+ return 1;
1841
+ }
1842
+ const hello = {
1843
+ handle: resolveHandle(),
1844
+ league: profile.league,
1845
+ harness: profile.harness
1846
+ };
1847
+ process2.stdout.write("\n");
1848
+ process2.stdout.write(` ${LIVE_NOTICE}
1849
+ `);
1850
+ process2.stdout.write(
1851
+ ` looking for ${target} in your league${any ? " (+ every league)" : " + adjacent"}\u2026
1852
+ `
1853
+ );
1854
+ let found = false;
1855
+ const { topics, acceptLeague } = discoveryScope(profile.league, any);
1856
+ const session = await startDiscovery({
1857
+ hello,
1858
+ topics,
1859
+ acceptLeague,
1860
+ isBlocked: blockedChecker(),
1861
+ onPeer: (peer) => {
1862
+ const { qual } = peerDirection(profile.league, peer.league);
1863
+ if (sameHandle(peer.handle, target)) {
1864
+ found = true;
1865
+ process2.stdout.write(` \u2605 found ${peer.handle} (${peer.league}${qual} \xB7 ${peer.harness})
1866
+ `);
1867
+ } else {
1868
+ process2.stdout.write(` + ${peer.handle} (${peer.league}${qual}) \u2014 not your target
1869
+ `);
1870
+ }
1871
+ }
1872
+ });
1873
+ process2.stdout.write(
1874
+ ` topic: ${TOPIC_PREFIX}${profile.league} \u2192 ${session.topic.toString("hex").slice(0, 12)}\u2026${topics.length > 1 ? ` (+${topics.length - 1} more)` : ""}
1875
+ `
1876
+ );
1877
+ process2.stdout.write(" (Ctrl+C to stop)\n\n");
1878
+ await new Promise((resolve) => {
1879
+ process2.once("SIGINT", () => resolve());
1880
+ process2.once("SIGTERM", () => resolve());
1881
+ });
1882
+ process2.stdout.write("\n leaving the swarm\u2026\n");
1883
+ await session.close();
1884
+ if (!found) {
1885
+ process2.stdout.write(` \u2717 ${target} was not found this session
1886
+
1887
+ `);
1888
+ } else {
1889
+ process2.stdout.write("\n");
1890
+ }
1891
+ return 0;
1892
+ }
1893
+ async function cmdBlock(arg) {
1894
+ if (arg === void 0 || arg.trim() === "") {
1895
+ process2.stderr.write("usage: vibedating block <@handle>\n");
1896
+ return 1;
1897
+ }
1898
+ const canonical = normalizeHandle(arg);
1899
+ if (canonical === null) {
1900
+ process2.stderr.write(`invalid handle: ${arg}
1901
+ `);
1902
+ return 1;
1903
+ }
1904
+ const { blocked, changed } = addBlock(canonical);
1905
+ process2.stdout.write(
1906
+ changed ? ` blocked ${canonical} (saved to ~/.vibedating/blocklist.json)
1907
+ ` : ` ${canonical} is already blocked
1908
+ `
1909
+ );
1910
+ process2.stdout.write(` ${blocked.length} handle${blocked.length === 1 ? "" : "s"} blocked
1911
+ `);
1912
+ return 0;
1913
+ }
1914
+ async function cmdUnblock(arg) {
1915
+ if (arg === void 0 || arg.trim() === "") {
1916
+ process2.stderr.write("usage: vibedating unblock <@handle>\n");
1917
+ return 1;
1918
+ }
1919
+ const canonical = normalizeHandle(arg);
1920
+ if (canonical === null) {
1921
+ process2.stderr.write(`invalid handle: ${arg}
1922
+ `);
1923
+ return 1;
1924
+ }
1925
+ const { blocked, changed } = removeBlock(canonical);
1926
+ process2.stdout.write(
1927
+ changed ? ` unblocked ${canonical}
1928
+ ` : ` ${canonical} was not blocked
1929
+ `
1930
+ );
1931
+ process2.stdout.write(` ${blocked.length} handle${blocked.length === 1 ? "" : "s"} blocked
1932
+ `);
1933
+ return 0;
1934
+ }
1935
+ async function cmdBlocklist() {
1936
+ const blocked = loadBlocklist();
1937
+ if (blocked.length === 0) {
1938
+ process2.stdout.write(" (blocklist is empty)\n");
1939
+ return 0;
1940
+ }
1941
+ process2.stdout.write(` ${blocked.length} blocked handle${blocked.length === 1 ? "" : "s"}:
1942
+ `);
1943
+ for (const h of blocked) process2.stdout.write(` ${h}
1944
+ `);
1945
+ return 0;
1946
+ }
1726
1947
  var HELP = `vibedating ${VERSION} \u2014 dating by tokens (local-first)
1727
1948
 
1728
1949
  Usage:
1729
1950
  vibedating connect Read your usage, compute + print your league
1730
1951
  vibedating matches [--live] List candidates in your league (live peers if any)
1731
1952
  vibedating discover [--live] [--any] Find live peers over the DHT (your league + adjacent; --any = everyone)
1732
- vibedating live [--dating] [--any] Live chat (your league + adjacent; --any = everyone; /next or --dating pick)
1953
+ vibedating live [--dating] [--any] [--to @handle] Live chat (your league + adjacent; --any = everyone; /next or --dating pick; --to targets one peer)
1954
+ vibedating find <@handle> [--any] Search the DHT for one specific handle (\u2605 highlights a match)
1955
+ vibedating handle [@name] Print or set your handle (persisted; a leading '@' is optional)
1956
+ vibedating block <@handle> Block a handle \u2014 their hello is dropped (never recorded/paired)
1957
+ vibedating unblock <@handle> Remove a handle from the blocklist
1958
+ vibedating blocklist List blocked handles
1733
1959
  vibedating open [--port N] Serve the local web app (default: random port)
1734
1960
  + live A/V video with connected same-league peers
1735
1961
  vibedating mcp Run the stdio MCP server (profile, matches)
@@ -1744,11 +1970,12 @@ Privacy:
1744
1970
  Matching:
1745
1971
  discover/live match your league + adjacent (\xB11) tiers by default, so thin
1746
1972
  leagues and cross-league friends still connect. --any matches every league.
1973
+ find / live --to look for one specific handle instead of the first random peer.
1747
1974
 
1748
1975
  Env:
1749
1976
  VIBEDATING_TOKENS=<n> Self-report a token count (e.g. 23400000 or 12M)
1750
1977
  VIBEDATING_HARNESS=<h> Harness id (claude-code, codex, \u2026)
1751
- VIBEDATING_HANDLE=<@id> Display handle
1978
+ VIBEDATING_HANDLE=<@id> Display handle (one-off override; not persisted)
1752
1979
  `;
1753
1980
  async function main(argv) {
1754
1981
  const parsed = parseArgs(argv);
@@ -1763,6 +1990,14 @@ async function main(argv) {
1763
1990
  return 0;
1764
1991
  case "connect":
1765
1992
  return cmdConnect();
1993
+ case "handle":
1994
+ return cmdHandle(parsed.arg);
1995
+ case "block":
1996
+ return cmdBlock(parsed.arg);
1997
+ case "unblock":
1998
+ return cmdUnblock(parsed.arg);
1999
+ case "blocklist":
2000
+ return cmdBlocklist();
1766
2001
  case "matches":
1767
2002
  return cmdMatches(parsed.live);
1768
2003
  case "discover":
@@ -1770,7 +2005,9 @@ async function main(argv) {
1770
2005
  case "open":
1771
2006
  return cmdOpen(parsed.port);
1772
2007
  case "live":
1773
- return cmdLive(parsed.dating, parsed.any);
2008
+ return cmdLive(parsed.dating, parsed.any, parsed.to);
2009
+ case "find":
2010
+ return cmdFind(parsed.arg, parsed.any);
1774
2011
  case "mcp":
1775
2012
  await runMcp();
1776
2013
  return 0;
package/dist/index.d.ts CHANGED
@@ -160,6 +160,13 @@ interface DiscoveryOptions {
160
160
  * peers that arrive on a shared topic.
161
161
  */
162
162
  readonly acceptLeague?: (peerLeague: string) => boolean;
163
+ /**
164
+ * Predicate over an incoming peer's advertised handle. A blocked peer's hello
165
+ * is DROPPED exactly like a wrong-league one — never recorded to peers.json,
166
+ * never notified, never handed to `onLink`/pairing. Default: nothing blocked.
167
+ * The CLI passes one backed by the persisted blocklist (~/.vibedating).
168
+ */
169
+ readonly isBlocked?: (handle: string) => boolean;
163
170
  /** DHT bootstrap nodes; omit for the public DHT. Tests pass a local testnet. */
164
171
  readonly bootstrap?: ReadonlyArray<{
165
172
  readonly host: string;
package/dist/index.js CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  serializeHandshake,
22
22
  startDiscovery,
23
23
  tryReadVerifiedUsage
24
- } from "./chunk-VBLFAOV3.js";
24
+ } from "./chunk-3VT4EOBX.js";
25
25
  export {
26
26
  BELOW_LEAGUE,
27
27
  CANDIDATES,
package/dist/mcp.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  runMcp
3
- } from "./chunk-DMA7MSN7.js";
4
- import "./chunk-VBLFAOV3.js";
3
+ } from "./chunk-OVO2CQ7X.js";
4
+ import "./chunk-3VT4EOBX.js";
5
5
  export {
6
6
  runMcp
7
7
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibedate",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Dating by tokens — matched by usage league across agentic CLIs. Raw usage stays local; only your league is shared. CLI + local web app + MCP. Local-first.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -37,7 +37,8 @@
37
37
  "hyperdht": "^6.33.0",
38
38
  "tsup": "^8.0.0",
39
39
  "typescript": "^5.4.0",
40
- "vitest": "^1.6.0"
40
+ "vitest": "^1.6.0",
41
+ "werift": "^0.24.1"
41
42
  },
42
43
  "homepage": "https://github.com/pooriaarab/vibedating#readme",
43
44
  "repository": {