vibedate 0.3.0 → 0.4.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.
@@ -2,7 +2,7 @@ import {
2
2
  CANDIDATES,
3
3
  loadProfile,
4
4
  matches
5
- } from "./chunk-VBLFAOV3.js";
5
+ } from "./chunk-43IYNWES.js";
6
6
 
7
7
  // src/mcp.ts
8
8
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -29,7 +29,7 @@ async function runMcp() {
29
29
  `handle: ${p.handle}`,
30
30
  `harness: ${p.harness}`,
31
31
  `league: ${p.league} League`,
32
- `verified: ${p.verified ? "true (read-only OAuth)" : "false (self-reported)"}`,
32
+ `verified: ${p.verified ? "true (real local usage)" : "false (self-reported or demo)"}`,
33
33
  "privacy: raw token usage is local-only; only the league bucket is shared."
34
34
  ];
35
35
  return { content: [textBlock(lines.join("\n"))] };
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,36 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runMcp
4
- } from "./chunk-DMA7MSN7.js";
4
+ } from "./chunk-ZB56ZBAR.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,
20
+ loadOrCreateIdentity,
17
21
  loadPeers,
18
22
  loadProfile,
19
23
  matches,
24
+ normalizeHandle,
20
25
  parseFrame,
21
26
  readUsage,
27
+ removeBlock,
28
+ resolveHandle,
29
+ sameHandle,
30
+ saveHandle,
31
+ signHelloClaims,
22
32
  startDiscovery
23
- } from "./chunk-VBLFAOV3.js";
33
+ } from "./chunk-43IYNWES.js";
24
34
 
25
35
  // src/cli.ts
26
36
  import readline from "readline";
@@ -1390,21 +1400,45 @@ async function handle(req, res, opts) {
1390
1400
  }
1391
1401
 
1392
1402
  // src/cli.ts
1393
- var VERSION = "0.3.0";
1403
+ var VERSION = "0.4.0";
1394
1404
  function parsePort(raw) {
1395
1405
  const n = Number(raw);
1396
1406
  if (!Number.isInteger(n) || n < 1 || n > 65535) return void 0;
1397
1407
  return n;
1398
1408
  }
1399
1409
  function parseArgs(argv) {
1400
- let out = { command: null, port: void 0, live: false, dating: false, any: false };
1410
+ let out = {
1411
+ command: null,
1412
+ port: void 0,
1413
+ live: false,
1414
+ dating: false,
1415
+ any: false,
1416
+ arg: void 0,
1417
+ to: void 0
1418
+ };
1401
1419
  for (let i = 0; i < argv.length; i++) {
1402
1420
  const a = argv[i];
1403
1421
  if (a === "--version" || a === "-v") {
1404
- return { command: "version", port: void 0, live: false, dating: false, any: false };
1422
+ return {
1423
+ command: "version",
1424
+ port: void 0,
1425
+ live: false,
1426
+ dating: false,
1427
+ any: false,
1428
+ arg: void 0,
1429
+ to: void 0
1430
+ };
1405
1431
  }
1406
1432
  if (a === "--help" || a === "-h") {
1407
- return { command: "help", port: void 0, live: false, dating: false, any: false };
1433
+ return {
1434
+ command: "help",
1435
+ port: void 0,
1436
+ live: false,
1437
+ dating: false,
1438
+ any: false,
1439
+ arg: void 0,
1440
+ to: void 0
1441
+ };
1408
1442
  }
1409
1443
  if (a === "--live") {
1410
1444
  out = { ...out, live: true };
@@ -1432,10 +1466,24 @@ function parseArgs(argv) {
1432
1466
  if (p !== void 0) out = { ...out, port: p };
1433
1467
  continue;
1434
1468
  }
1469
+ if (a === "--to") {
1470
+ const next = argv[i + 1];
1471
+ if (next !== void 0) {
1472
+ out = { ...out, to: next };
1473
+ i++;
1474
+ }
1475
+ continue;
1476
+ }
1477
+ if (a.startsWith("--to=")) {
1478
+ out = { ...out, to: a.slice("--to=".length) };
1479
+ continue;
1480
+ }
1435
1481
  if (a.startsWith("-")) continue;
1436
- const known = a === "connect" || a === "matches" || a === "discover" || a === "open" || a === "live" || a === "mcp" || a === "help" ? a : null;
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;
1437
1483
  if (known !== null && out.command === null) {
1438
1484
  out = { ...out, command: known };
1485
+ } else if (out.arg === void 0) {
1486
+ out = { ...out, arg: a };
1439
1487
  }
1440
1488
  }
1441
1489
  return out;
@@ -1443,6 +1491,21 @@ function parseArgs(argv) {
1443
1491
  function leagueLabel(name) {
1444
1492
  return name === "below-1M" ? "below 1M (not yet in a league)" : `${name} League`;
1445
1493
  }
1494
+ function formatTokens(n) {
1495
+ const trim = (v) => String(Math.round(v * 10) / 10);
1496
+ const abs = Math.abs(n);
1497
+ if (abs >= 1e9) return `${trim(n / 1e9)}B`;
1498
+ if (abs >= 1e6) return `${trim(n / 1e6)}M`;
1499
+ if (abs >= 1e3) return `${trim(n / 1e3)}k`;
1500
+ return String(n);
1501
+ }
1502
+ function verificationText(snapshot) {
1503
+ if (snapshot.source === "real") {
1504
+ return `verified: real usage \u2014 ${formatTokens(snapshot.totalTokens)} tokens from ${snapshot.harness} logs`;
1505
+ }
1506
+ if (snapshot.source === "self-report") return "self-reported (unverified)";
1507
+ return "demo (unverified)";
1508
+ }
1446
1509
  function discoveryScope(myLeague, any) {
1447
1510
  const names = any ? allLeagueNames() : leaguesWithin(myLeague, 1);
1448
1511
  const ordered = [myLeague, ...names.filter((n) => n !== myLeague)];
@@ -1455,31 +1518,73 @@ function discoveryScope(myLeague, any) {
1455
1518
  acceptLeague: (peerLeague) => accepted.has(peerLeague)
1456
1519
  };
1457
1520
  }
1521
+ function blockedChecker() {
1522
+ return (handle2) => isBlocked(handle2);
1523
+ }
1458
1524
  function peerDirection(myLeague, peerLeague, sameBullet = "+") {
1459
1525
  const d = leagueIndex(peerLeague) - leagueIndex(myLeague);
1460
1526
  if (d > 0) return { bullet: "\u2191", qual: " \xB7 higher league" };
1461
1527
  if (d < 0) return { bullet: "\u2193", qual: " \xB7 lower league" };
1462
1528
  return { bullet: sameBullet, qual: "" };
1463
1529
  }
1530
+ function buildHello(profile) {
1531
+ const claims = {
1532
+ handle: resolveHandle(),
1533
+ league: profile.league,
1534
+ harness: profile.harness,
1535
+ verified: profile.verified
1536
+ };
1537
+ return { ...claims, ...signHelloClaims(loadOrCreateIdentity(), claims) };
1538
+ }
1539
+ function usageMark(peer) {
1540
+ return peer.verified === true ? "\u2713" : "~";
1541
+ }
1542
+ function idMark(peer) {
1543
+ return peer.identityVerified === true ? " \u{1F511}" : "";
1544
+ }
1545
+ var MARKS_LEGEND = "marks: \u2713 usage verified (real local logs) \xB7 ~ unverified (self-report/demo/legacy) \xB7 \u{1F511} identity-verified (signed hello)";
1464
1546
  async function cmdConnect() {
1465
1547
  const harness = process2.env["VIBEDATING_HARNESS"] ?? "claude-code";
1466
- const handle2 = process2.env["VIBEDATING_HANDLE"] ?? "@you";
1548
+ const handle2 = resolveHandle();
1467
1549
  const snapshot = await readUsage(harness);
1468
1550
  const profile = connectProfile(snapshot, handle2);
1551
+ const identity = loadOrCreateIdentity();
1469
1552
  const lg = league(snapshot.totalTokens);
1470
1553
  process2.stdout.write("\n");
1471
1554
  process2.stdout.write(` ${leagueLabel(lg.name)}
1472
1555
  `);
1473
1556
  process2.stdout.write(` handle: ${profile.handle} \xB7 harness: ${profile.harness}
1474
1557
  `);
1475
- process2.stdout.write(
1476
- ` verification: ${profile.verified ? "verified (read-only OAuth)" : "self-reported"}
1477
- `
1478
- );
1558
+ process2.stdout.write(` verification: ${verificationText(snapshot)}
1559
+ `);
1560
+ process2.stdout.write(` identity: ed25519 ${identity.publicKeyHex.slice(0, 12)}\u2026 \u2014 signs your hello (\u{1F511})
1561
+ `);
1479
1562
  process2.stdout.write("\n");
1480
1563
  process2.stdout.write(" \u2022 raw usage stays local \xB7 only league shared\n\n");
1481
1564
  return 0;
1482
1565
  }
1566
+ async function cmdHandle(arg) {
1567
+ if (arg === void 0 || arg.trim() === "") {
1568
+ const handle2 = resolveHandle();
1569
+ process2.stdout.write(`${handle2}
1570
+ `);
1571
+ const env = process2.env["VIBEDATING_HANDLE"];
1572
+ if (env !== void 0 && env.trim() !== "" && normalizeHandle(env) !== null) {
1573
+ process2.stdout.write(" (env VIBEDATING_HANDLE overrides the persisted handle for this run)\n");
1574
+ }
1575
+ return 0;
1576
+ }
1577
+ try {
1578
+ const canonical = saveHandle(arg);
1579
+ process2.stdout.write(` handle set \u2192 ${canonical} (saved to ~/.vibedating/handle.json)
1580
+ `);
1581
+ return 0;
1582
+ } catch (err) {
1583
+ process2.stderr.write(`${err instanceof Error ? err.message : String(err)}
1584
+ `);
1585
+ return 1;
1586
+ }
1587
+ }
1483
1588
  async function cmdMatches(live) {
1484
1589
  const profile = loadProfile();
1485
1590
  if (!profile) {
@@ -1502,11 +1607,13 @@ async function cmdMatches(live) {
1502
1607
  `);
1503
1608
  process2.stdout.write(
1504
1609
  `${livePeers.length} live peer${livePeers.length === 1 ? "" : "s"} in range (discovered over the DHT):
1505
-
1506
1610
  `
1507
1611
  );
1612
+ process2.stdout.write(` ${MARKS_LEGEND}
1613
+
1614
+ `);
1508
1615
  for (const p of livePeers) {
1509
- process2.stdout.write(` ${p.handle.padEnd(28)} ${p.league} \xB7 ${p.harness}
1616
+ process2.stdout.write(` ${p.handle.padEnd(28)} ${p.league} \xB7 ${p.harness} ${usageMark(p)}${idMark(p)}
1510
1617
  `);
1511
1618
  }
1512
1619
  process2.stdout.write("\n");
@@ -1542,27 +1649,26 @@ async function cmdDiscover(live, any) {
1542
1649
  if (live && !canShareLive()) grantLiveConsent();
1543
1650
  if (!canShareLive()) {
1544
1651
  process2.stderr.write(
1545
- "Live discovery is off. It shares ONLY your handle + league + harness (never raw usage)\nwith same-league peers on the public DHT. Opt in: `vibedating discover --live`\n"
1652
+ "Live discovery is off. It shares ONLY your handle + league + harness + verified flag + identity pubkey\n(never raw usage) with same-league peers on the public DHT. Opt in: `vibedating discover --live`\n"
1546
1653
  );
1547
1654
  return 1;
1548
1655
  }
1549
- const hello = {
1550
- handle: profile.handle,
1551
- league: profile.league,
1552
- harness: profile.harness
1553
- };
1656
+ const hello = buildHello(profile);
1554
1657
  process2.stdout.write("\n");
1555
1658
  process2.stdout.write(` ${LIVE_NOTICE}
1659
+ `);
1660
+ process2.stdout.write(` ${MARKS_LEGEND}
1556
1661
  `);
1557
1662
  const { topics, acceptLeague } = discoveryScope(profile.league, any);
1558
1663
  const session = await startDiscovery({
1559
1664
  hello,
1560
1665
  topics,
1561
1666
  acceptLeague,
1667
+ isBlocked: blockedChecker(),
1562
1668
  onPeer: (peer, isNew) => {
1563
1669
  const { bullet, qual } = peerDirection(profile.league, peer.league);
1564
1670
  process2.stdout.write(
1565
- ` ${bullet} ${peer.handle} (${peer.league}${qual} \xB7 ${peer.harness})${isNew ? " \u2190 new match" : ""}
1671
+ ` ${bullet} ${peer.handle} (${peer.league}${qual} \xB7 ${peer.harness}) ${usageMark(peer)}${idMark(peer)}${isNew ? " \u2190 new match" : ""}
1566
1672
  `
1567
1673
  );
1568
1674
  }
@@ -1605,12 +1711,8 @@ async function cmdOpen(port) {
1605
1711
  process2.stdout.write(`
1606
1712
  ${LIVE_NOTICE}
1607
1713
  `);
1608
- const hello = {
1609
- handle: profile.handle,
1610
- league: profile.league,
1611
- harness: profile.harness
1612
- };
1613
- void startDiscovery({ hello, onLink: (link) => live.addLink(link) }).then((s) => {
1714
+ const hello = buildHello(profile);
1715
+ void startDiscovery({ hello, isBlocked: blockedChecker(), onLink: (link) => live.addLink(link) }).then((s) => {
1614
1716
  session = s;
1615
1717
  }).catch(() => {
1616
1718
  });
@@ -1634,27 +1736,32 @@ async function cmdOpen(port) {
1634
1736
  await new Promise((resolve) => started.server.close(() => resolve()));
1635
1737
  return 0;
1636
1738
  }
1637
- async function cmdLive(dating, any) {
1739
+ async function cmdLive(dating, any, to) {
1638
1740
  const profile = loadProfile();
1639
1741
  if (!profile) {
1640
1742
  process2.stderr.write("Not connected yet. Run `vibedating connect` first.\n");
1641
1743
  return 1;
1642
1744
  }
1745
+ const target = to !== void 0 ? normalizeHandle(to) : null;
1746
+ if (to !== void 0 && target === null) {
1747
+ process2.stderr.write(`invalid target handle: ${to}
1748
+ `);
1749
+ return 1;
1750
+ }
1643
1751
  if (!canShareLive()) grantLiveConsent();
1644
1752
  if (!canShareLive()) {
1645
1753
  process2.stderr.write("Could not enable live discovery. Try `vibedating discover --live`.\n");
1646
1754
  return 1;
1647
1755
  }
1648
- const hello = {
1649
- handle: profile.handle,
1650
- league: profile.league,
1651
- harness: profile.harness
1652
- };
1756
+ const hello = buildHello(profile);
1653
1757
  process2.stdout.write("\n");
1654
1758
  process2.stdout.write(` ${LIVE_NOTICE}
1759
+ `);
1760
+ process2.stdout.write(` ${MARKS_LEGEND}
1655
1761
  `);
1656
1762
  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"
1763
+ target !== null ? ` targeted \u2014 auto-opening ${target} when they connect (/quit to stop)
1764
+ ` : dating ? " dating mode \u2014 /open <handle> to pick a peer\n" : " omegle mode \u2014 /next to roll a new peer\n"
1658
1765
  );
1659
1766
  const pairing = createPairing();
1660
1767
  pairing.onMatch((link) => {
@@ -1664,7 +1771,7 @@ async function cmdLive(dating, any) {
1664
1771
  }
1665
1772
  const { qual } = peerDirection(profile.league, link.hello.league);
1666
1773
  process2.stdout.write(
1667
- ` \xB7 matched ${link.hello.handle} (${link.hello.league}${qual} \xB7 ${link.hello.harness})
1774
+ ` \xB7 matched ${link.hello.handle} (${link.hello.league}${qual} \xB7 ${link.hello.harness}) ${usageMark(link.hello)}${idMark(link.hello)}
1668
1775
  `
1669
1776
  );
1670
1777
  link.onMessage((m) => {
@@ -1677,7 +1784,23 @@ async function cmdLive(dating, any) {
1677
1784
  hello,
1678
1785
  topics,
1679
1786
  acceptLeague,
1680
- onLink: (link) => pairing.add(link)
1787
+ isBlocked: blockedChecker(),
1788
+ onLink: (link) => {
1789
+ if (target !== null && !sameHandle(link.hello.handle, target)) {
1790
+ const { qual } = peerDirection(profile.league, link.hello.league);
1791
+ process2.stdout.write(
1792
+ ` + ${link.hello.handle} (${link.hello.league}${qual}) ${usageMark(link.hello)}${idMark(link.hello)} \u2014 not your target
1793
+ `
1794
+ );
1795
+ link.close();
1796
+ return;
1797
+ }
1798
+ if (target !== null) {
1799
+ process2.stdout.write(` \u2605 found ${link.hello.handle} \u2014 auto-opening
1800
+ `);
1801
+ }
1802
+ pairing.add(link);
1803
+ }
1681
1804
  });
1682
1805
  process2.stdout.write(
1683
1806
  ` topic: ${TOPIC_PREFIX}${profile.league} \u2192 ${session.topic.toString("hex").slice(0, 12)}\u2026${topics.length > 1 ? ` (+${topics.length - 1} more)` : ""}
@@ -1723,13 +1846,146 @@ async function cmdLive(dating, any) {
1723
1846
  process2.stdout.write("\n");
1724
1847
  return 0;
1725
1848
  }
1849
+ async function cmdFind(targetArg, any) {
1850
+ if (targetArg === void 0 || targetArg.trim() === "") {
1851
+ process2.stderr.write("usage: vibedating find <@handle> [--any]\n");
1852
+ return 1;
1853
+ }
1854
+ const target = normalizeHandle(targetArg);
1855
+ if (target === null) {
1856
+ process2.stderr.write(`invalid handle: ${targetArg}
1857
+ `);
1858
+ return 1;
1859
+ }
1860
+ const profile = loadProfile();
1861
+ if (!profile) {
1862
+ process2.stderr.write("Not connected yet. Run `vibedating connect` first.\n");
1863
+ return 1;
1864
+ }
1865
+ if (!canShareLive()) grantLiveConsent();
1866
+ if (!canShareLive()) {
1867
+ process2.stderr.write("Could not enable live discovery. Try `vibedating discover --live`.\n");
1868
+ return 1;
1869
+ }
1870
+ const hello = buildHello(profile);
1871
+ process2.stdout.write("\n");
1872
+ process2.stdout.write(` ${LIVE_NOTICE}
1873
+ `);
1874
+ process2.stdout.write(` ${MARKS_LEGEND}
1875
+ `);
1876
+ process2.stdout.write(
1877
+ ` looking for ${target} in your league${any ? " (+ every league)" : " + adjacent"}\u2026
1878
+ `
1879
+ );
1880
+ let found = false;
1881
+ const { topics, acceptLeague } = discoveryScope(profile.league, any);
1882
+ const session = await startDiscovery({
1883
+ hello,
1884
+ topics,
1885
+ acceptLeague,
1886
+ isBlocked: blockedChecker(),
1887
+ onPeer: (peer) => {
1888
+ const { qual } = peerDirection(profile.league, peer.league);
1889
+ if (sameHandle(peer.handle, target)) {
1890
+ found = true;
1891
+ process2.stdout.write(
1892
+ ` \u2605 found ${peer.handle} (${peer.league}${qual} \xB7 ${peer.harness}) ${usageMark(peer)}${idMark(peer)}
1893
+ `
1894
+ );
1895
+ } else {
1896
+ process2.stdout.write(
1897
+ ` + ${peer.handle} (${peer.league}${qual}) ${usageMark(peer)}${idMark(peer)} \u2014 not your target
1898
+ `
1899
+ );
1900
+ }
1901
+ }
1902
+ });
1903
+ process2.stdout.write(
1904
+ ` topic: ${TOPIC_PREFIX}${profile.league} \u2192 ${session.topic.toString("hex").slice(0, 12)}\u2026${topics.length > 1 ? ` (+${topics.length - 1} more)` : ""}
1905
+ `
1906
+ );
1907
+ process2.stdout.write(" (Ctrl+C to stop)\n\n");
1908
+ await new Promise((resolve) => {
1909
+ process2.once("SIGINT", () => resolve());
1910
+ process2.once("SIGTERM", () => resolve());
1911
+ });
1912
+ process2.stdout.write("\n leaving the swarm\u2026\n");
1913
+ await session.close();
1914
+ if (!found) {
1915
+ process2.stdout.write(` \u2717 ${target} was not found this session
1916
+
1917
+ `);
1918
+ } else {
1919
+ process2.stdout.write("\n");
1920
+ }
1921
+ return 0;
1922
+ }
1923
+ async function cmdBlock(arg) {
1924
+ if (arg === void 0 || arg.trim() === "") {
1925
+ process2.stderr.write("usage: vibedating block <@handle>\n");
1926
+ return 1;
1927
+ }
1928
+ const canonical = normalizeHandle(arg);
1929
+ if (canonical === null) {
1930
+ process2.stderr.write(`invalid handle: ${arg}
1931
+ `);
1932
+ return 1;
1933
+ }
1934
+ const { blocked, changed } = addBlock(canonical);
1935
+ process2.stdout.write(
1936
+ changed ? ` blocked ${canonical} (saved to ~/.vibedating/blocklist.json)
1937
+ ` : ` ${canonical} is already blocked
1938
+ `
1939
+ );
1940
+ process2.stdout.write(` ${blocked.length} handle${blocked.length === 1 ? "" : "s"} blocked
1941
+ `);
1942
+ return 0;
1943
+ }
1944
+ async function cmdUnblock(arg) {
1945
+ if (arg === void 0 || arg.trim() === "") {
1946
+ process2.stderr.write("usage: vibedating unblock <@handle>\n");
1947
+ return 1;
1948
+ }
1949
+ const canonical = normalizeHandle(arg);
1950
+ if (canonical === null) {
1951
+ process2.stderr.write(`invalid handle: ${arg}
1952
+ `);
1953
+ return 1;
1954
+ }
1955
+ const { blocked, changed } = removeBlock(canonical);
1956
+ process2.stdout.write(
1957
+ changed ? ` unblocked ${canonical}
1958
+ ` : ` ${canonical} was not blocked
1959
+ `
1960
+ );
1961
+ process2.stdout.write(` ${blocked.length} handle${blocked.length === 1 ? "" : "s"} blocked
1962
+ `);
1963
+ return 0;
1964
+ }
1965
+ async function cmdBlocklist() {
1966
+ const blocked = loadBlocklist();
1967
+ if (blocked.length === 0) {
1968
+ process2.stdout.write(" (blocklist is empty)\n");
1969
+ return 0;
1970
+ }
1971
+ process2.stdout.write(` ${blocked.length} blocked handle${blocked.length === 1 ? "" : "s"}:
1972
+ `);
1973
+ for (const h of blocked) process2.stdout.write(` ${h}
1974
+ `);
1975
+ return 0;
1976
+ }
1726
1977
  var HELP = `vibedating ${VERSION} \u2014 dating by tokens (local-first)
1727
1978
 
1728
1979
  Usage:
1729
1980
  vibedating connect Read your usage, compute + print your league
1730
1981
  vibedating matches [--live] List candidates in your league (live peers if any)
1731
1982
  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)
1983
+ vibedating live [--dating] [--any] [--to @handle] Live chat (your league + adjacent; --any = everyone; /next or --dating pick; --to targets one peer)
1984
+ vibedating find <@handle> [--any] Search the DHT for one specific handle (\u2605 highlights a match)
1985
+ vibedating handle [@name] Print or set your handle (persisted; a leading '@' is optional)
1986
+ vibedating block <@handle> Block a handle \u2014 their hello is dropped (never recorded/paired)
1987
+ vibedating unblock <@handle> Remove a handle from the blocklist
1988
+ vibedating blocklist List blocked handles
1733
1989
  vibedating open [--port N] Serve the local web app (default: random port)
1734
1990
  + live A/V video with connected same-league peers
1735
1991
  vibedating mcp Run the stdio MCP server (profile, matches)
@@ -1739,16 +1995,20 @@ Usage:
1739
1995
  Privacy:
1740
1996
  Raw token usage is read and stored LOCALLY (~/.vibedating). Only the league
1741
1997
  bucket is ever shared. Live discovery (off by default) shares ONLY your
1742
- handle + league + harness with same-league peers \u2014 opt in with --live.
1998
+ handle + league + harness + verified flag + identity pubkey (an ed25519 key
1999
+ generated on first connect, stored 0600 in ~/.vibedating/identity.json) with
2000
+ same-league peers \u2014 opt in with --live. Peers are marked: \u2713 usage verified
2001
+ (real local logs) \xB7 ~ unverified \xB7 \u{1F511} identity-verified (signed hello).
1743
2002
 
1744
2003
  Matching:
1745
2004
  discover/live match your league + adjacent (\xB11) tiers by default, so thin
1746
2005
  leagues and cross-league friends still connect. --any matches every league.
2006
+ find / live --to look for one specific handle instead of the first random peer.
1747
2007
 
1748
2008
  Env:
1749
2009
  VIBEDATING_TOKENS=<n> Self-report a token count (e.g. 23400000 or 12M)
1750
2010
  VIBEDATING_HARNESS=<h> Harness id (claude-code, codex, \u2026)
1751
- VIBEDATING_HANDLE=<@id> Display handle
2011
+ VIBEDATING_HANDLE=<@id> Display handle (one-off override; not persisted)
1752
2012
  `;
1753
2013
  async function main(argv) {
1754
2014
  const parsed = parseArgs(argv);
@@ -1763,6 +2023,14 @@ async function main(argv) {
1763
2023
  return 0;
1764
2024
  case "connect":
1765
2025
  return cmdConnect();
2026
+ case "handle":
2027
+ return cmdHandle(parsed.arg);
2028
+ case "block":
2029
+ return cmdBlock(parsed.arg);
2030
+ case "unblock":
2031
+ return cmdUnblock(parsed.arg);
2032
+ case "blocklist":
2033
+ return cmdBlocklist();
1766
2034
  case "matches":
1767
2035
  return cmdMatches(parsed.live);
1768
2036
  case "discover":
@@ -1770,7 +2038,9 @@ async function main(argv) {
1770
2038
  case "open":
1771
2039
  return cmdOpen(parsed.port);
1772
2040
  case "live":
1773
- return cmdLive(parsed.dating, parsed.any);
2041
+ return cmdLive(parsed.dating, parsed.any, parsed.to);
2042
+ case "find":
2043
+ return cmdFind(parsed.arg, parsed.any);
1774
2044
  case "mcp":
1775
2045
  await runMcp();
1776
2046
  return 0;