vibedate 0.2.1 → 0.3.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-6NWF2VD7.js";
5
+ } from "./chunk-VBLFAOV3.js";
6
6
 
7
7
  // src/mcp.ts
8
8
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -511,7 +511,8 @@ function recordPeer(hello, dir = defaultStateDir(), now = /* @__PURE__ */ new Da
511
511
  }
512
512
  async function startDiscovery(opts) {
513
513
  const { hello, stateDir = defaultStateDir(), onPeer, onLink, notify = vibeCoreNotify } = opts;
514
- const topic = opts.topic ?? leagueTopic(hello.league);
514
+ const topics = opts.topics ? [...opts.topics] : opts.topic !== void 0 ? [opts.topic] : [leagueTopic(hello.league)];
515
+ const acceptLeague = opts.acceptLeague ?? ((l) => l === hello.league);
515
516
  const { default: Hyperswarm } = await import("hyperswarm");
516
517
  const swarm = new Hyperswarm(opts.bootstrap === void 0 ? {} : { bootstrap: opts.bootstrap });
517
518
  await swarm.dht.fullyBootstrapped();
@@ -539,7 +540,7 @@ async function startDiscovery(opts) {
539
540
  league: frame.league,
540
541
  harness: frame.harness
541
542
  };
542
- if (peer.league !== hello.league) continue;
543
+ if (!acceptLeague(peer.league)) continue;
543
544
  peers.set(remoteKey, peer);
544
545
  const { isNew } = recordPeer(peer, stateDir);
545
546
  if (isNew) {
@@ -570,17 +571,22 @@ async function startDiscovery(opts) {
570
571
  socket.on("error", () => {
571
572
  });
572
573
  });
573
- const discovery = swarm.join(topic, { server: true, client: true });
574
- const ready = discovery.flushed().catch(() => void 0);
574
+ const discoveries = topics.map((t) => swarm.join(t, { server: true, client: true }));
575
+ const ready = Promise.all(
576
+ discoveries.map((d) => d.flushed().catch(() => void 0))
577
+ );
575
578
  await ready;
576
579
  const refresher = setInterval(() => {
577
- void discovery.refresh({ server: true, client: true }).catch(() => {
580
+ for (const d of discoveries) void d.refresh({ server: true, client: true }).catch(() => {
578
581
  });
579
582
  }, REFRESH_INTERVAL_MS);
580
583
  refresher.unref();
581
584
  let closed = false;
582
585
  return {
583
- topic,
586
+ topic: topics[0],
587
+ // primary (first) — kept for back-compat / display
588
+ topics,
589
+ // every joined topic (primary first)
584
590
  hello,
585
591
  peers,
586
592
  ready,
@@ -588,9 +594,11 @@ async function startDiscovery(opts) {
588
594
  if (closed) return;
589
595
  closed = true;
590
596
  clearInterval(refresher);
591
- try {
592
- await swarm.leave(topic);
593
- } catch {
597
+ for (const t of topics) {
598
+ try {
599
+ await swarm.leave(t);
600
+ } catch {
601
+ }
594
602
  }
595
603
  await swarm.destroy();
596
604
  }
@@ -619,6 +627,22 @@ function leagueIndex(name) {
619
627
  if (name === BELOW_LEAGUE) return -1;
620
628
  return LEAGUES.findIndex((l) => l.name === name);
621
629
  }
630
+ function allLeagueNames() {
631
+ return [BELOW_LEAGUE, ...LEAGUES.map((l) => l.name)];
632
+ }
633
+ function leaguesWithin(name, width) {
634
+ const w = Math.max(0, Math.trunc(width));
635
+ if (name === BELOW_LEAGUE) {
636
+ return [BELOW_LEAGUE, ...LEAGUES.slice(0, w).map((l) => l.name)];
637
+ }
638
+ const center = leagueIndex(name);
639
+ if (center < 0) return [];
640
+ const lo = Math.max(0, center - w);
641
+ const hi = Math.min(LEAGUES.length - 1, center + w);
642
+ const out = [];
643
+ for (let i = lo; i <= hi; i++) out.push(LEAGUES[i].name);
644
+ return out;
645
+ }
622
646
  var TOKENS_ENV = "VIBEDATING_TOKENS";
623
647
  var DEMO_TOTAL_TOKENS = 234e5;
624
648
  var MS_PER_DAY = 864e5;
@@ -733,6 +757,8 @@ export {
733
757
  BELOW_LEAGUE,
734
758
  league,
735
759
  leagueIndex,
760
+ allLeagueNames,
761
+ leaguesWithin,
736
762
  TOKENS_ENV,
737
763
  DEMO_TOTAL_TOKENS,
738
764
  readUsage,
package/dist/cli.d.ts CHANGED
@@ -9,6 +9,8 @@ interface ParsedArgs {
9
9
  readonly live: boolean;
10
10
  /** `live --dating`: pick-a-handle mode vs omegle auto-pair. Default false. */
11
11
  readonly dating: boolean;
12
+ /** `discover --any` / `live --any`: match every league (not just ±1). Default false. */
13
+ readonly any: boolean;
12
14
  }
13
15
  /**
14
16
  * Parse argv (the slice AFTER the program name) into a command + options.
package/dist/cli.js CHANGED
@@ -1,23 +1,26 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runMcp
4
- } from "./chunk-5ZJIPMU6.js";
4
+ } from "./chunk-DMA7MSN7.js";
5
5
  import {
6
6
  CANDIDATES,
7
7
  LIVE_NOTICE,
8
8
  TOPIC_PREFIX,
9
+ allLeagueNames,
9
10
  canShareLive,
10
11
  connectProfile,
11
12
  grantLiveConsent,
12
13
  league,
13
14
  leagueIndex,
15
+ leagueTopic,
16
+ leaguesWithin,
14
17
  loadPeers,
15
18
  loadProfile,
16
19
  matches,
17
20
  parseFrame,
18
21
  readUsage,
19
22
  startDiscovery
20
- } from "./chunk-6NWF2VD7.js";
23
+ } from "./chunk-VBLFAOV3.js";
21
24
 
22
25
  // src/cli.ts
23
26
  import readline from "readline";
@@ -1387,26 +1390,30 @@ async function handle(req, res, opts) {
1387
1390
  }
1388
1391
 
1389
1392
  // src/cli.ts
1390
- var VERSION = "0.2.1";
1393
+ var VERSION = "0.3.0";
1391
1394
  function parsePort(raw) {
1392
1395
  const n = Number(raw);
1393
1396
  if (!Number.isInteger(n) || n < 1 || n > 65535) return void 0;
1394
1397
  return n;
1395
1398
  }
1396
1399
  function parseArgs(argv) {
1397
- let out = { command: null, port: void 0, live: false, dating: false };
1400
+ let out = { command: null, port: void 0, live: false, dating: false, any: false };
1398
1401
  for (let i = 0; i < argv.length; i++) {
1399
1402
  const a = argv[i];
1400
1403
  if (a === "--version" || a === "-v") {
1401
- return { command: "version", port: void 0, live: false, dating: false };
1404
+ return { command: "version", port: void 0, live: false, dating: false, any: false };
1402
1405
  }
1403
1406
  if (a === "--help" || a === "-h") {
1404
- return { command: "help", port: void 0, live: false, dating: false };
1407
+ return { command: "help", port: void 0, live: false, dating: false, any: false };
1405
1408
  }
1406
1409
  if (a === "--live") {
1407
1410
  out = { ...out, live: true };
1408
1411
  continue;
1409
1412
  }
1413
+ if (a === "--any") {
1414
+ out = { ...out, any: true };
1415
+ continue;
1416
+ }
1410
1417
  if (a === "--dating") {
1411
1418
  out = { ...out, dating: true };
1412
1419
  continue;
@@ -1436,6 +1443,24 @@ function parseArgs(argv) {
1436
1443
  function leagueLabel(name) {
1437
1444
  return name === "below-1M" ? "below 1M (not yet in a league)" : `${name} League`;
1438
1445
  }
1446
+ function discoveryScope(myLeague, any) {
1447
+ const names = any ? allLeagueNames() : leaguesWithin(myLeague, 1);
1448
+ const ordered = [myLeague, ...names.filter((n) => n !== myLeague)];
1449
+ if (any) {
1450
+ return { topics: ordered.map(leagueTopic), acceptLeague: () => true };
1451
+ }
1452
+ const accepted = new Set(names);
1453
+ return {
1454
+ topics: ordered.map(leagueTopic),
1455
+ acceptLeague: (peerLeague) => accepted.has(peerLeague)
1456
+ };
1457
+ }
1458
+ function peerDirection(myLeague, peerLeague, sameBullet = "+") {
1459
+ const d = leagueIndex(peerLeague) - leagueIndex(myLeague);
1460
+ if (d > 0) return { bullet: "\u2191", qual: " \xB7 higher league" };
1461
+ if (d < 0) return { bullet: "\u2193", qual: " \xB7 lower league" };
1462
+ return { bullet: sameBullet, qual: "" };
1463
+ }
1439
1464
  async function cmdConnect() {
1440
1465
  const harness = process2.env["VIBEDATING_HARNESS"] ?? "claude-code";
1441
1466
  const handle2 = process2.env["VIBEDATING_HANDLE"] ?? "@you";
@@ -1508,7 +1533,7 @@ async function cmdMatches(live) {
1508
1533
  process2.stdout.write("\n");
1509
1534
  return 0;
1510
1535
  }
1511
- async function cmdDiscover(live) {
1536
+ async function cmdDiscover(live, any) {
1512
1537
  const profile = loadProfile();
1513
1538
  if (!profile) {
1514
1539
  process2.stderr.write("Not connected yet. Run `vibedating connect` first.\n");
@@ -1529,20 +1554,31 @@ async function cmdDiscover(live) {
1529
1554
  process2.stdout.write("\n");
1530
1555
  process2.stdout.write(` ${LIVE_NOTICE}
1531
1556
  `);
1557
+ const { topics, acceptLeague } = discoveryScope(profile.league, any);
1532
1558
  const session = await startDiscovery({
1533
1559
  hello,
1560
+ topics,
1561
+ acceptLeague,
1534
1562
  onPeer: (peer, isNew) => {
1563
+ const { bullet, qual } = peerDirection(profile.league, peer.league);
1535
1564
  process2.stdout.write(
1536
- ` + ${peer.handle} (${peer.league} \xB7 ${peer.harness})${isNew ? " \u2190 new match" : ""}
1565
+ ` ${bullet} ${peer.handle} (${peer.league}${qual} \xB7 ${peer.harness})${isNew ? " \u2190 new match" : ""}
1537
1566
  `
1538
1567
  );
1539
1568
  }
1540
1569
  });
1541
1570
  process2.stdout.write(
1542
- ` topic: ${TOPIC_PREFIX}${profile.league} \u2192 ${session.topic.toString("hex").slice(0, 12)}\u2026
1571
+ ` topic: ${TOPIC_PREFIX}${profile.league} \u2192 ${session.topic.toString("hex").slice(0, 12)}\u2026${topics.length > 1 ? ` (+${topics.length - 1} more)` : ""}
1543
1572
  `
1544
1573
  );
1545
- process2.stdout.write(" listening for same-league peers\u2026 (Ctrl+C to stop)\n\n");
1574
+ if (!any && session.peers.size === 0) {
1575
+ process2.stdout.write(
1576
+ " no one in your league yet \u2014 also listening to adjacent leagues (use --any to match anyone)\n"
1577
+ );
1578
+ }
1579
+ process2.stdout.write(
1580
+ any ? " listening for ANY league peers\u2026 (Ctrl+C to stop)\n\n" : " listening for same + adjacent-league peers\u2026 (Ctrl+C to stop)\n\n"
1581
+ );
1546
1582
  await new Promise((resolve) => {
1547
1583
  process2.once("SIGINT", () => resolve());
1548
1584
  process2.once("SIGTERM", () => resolve());
@@ -1598,7 +1634,7 @@ async function cmdOpen(port) {
1598
1634
  await new Promise((resolve) => started.server.close(() => resolve()));
1599
1635
  return 0;
1600
1636
  }
1601
- async function cmdLive(dating) {
1637
+ async function cmdLive(dating, any) {
1602
1638
  const profile = loadProfile();
1603
1639
  if (!profile) {
1604
1640
  process2.stderr.write("Not connected yet. Run `vibedating connect` first.\n");
@@ -1626,8 +1662,9 @@ async function cmdLive(dating) {
1626
1662
  process2.stdout.write(" \xB7 idle \u2014 no peer right now\n");
1627
1663
  return;
1628
1664
  }
1665
+ const { qual } = peerDirection(profile.league, link.hello.league);
1629
1666
  process2.stdout.write(
1630
- ` \xB7 matched ${link.hello.handle} (${link.hello.league} \xB7 ${link.hello.harness})
1667
+ ` \xB7 matched ${link.hello.handle} (${link.hello.league}${qual} \xB7 ${link.hello.harness})
1631
1668
  `
1632
1669
  );
1633
1670
  link.onMessage((m) => {
@@ -1635,12 +1672,15 @@ async function cmdLive(dating) {
1635
1672
  `);
1636
1673
  });
1637
1674
  });
1675
+ const { topics, acceptLeague } = discoveryScope(profile.league, any);
1638
1676
  const session = await startDiscovery({
1639
1677
  hello,
1678
+ topics,
1679
+ acceptLeague,
1640
1680
  onLink: (link) => pairing.add(link)
1641
1681
  });
1642
1682
  process2.stdout.write(
1643
- ` topic: ${TOPIC_PREFIX}${profile.league} \u2192 ${session.topic.toString("hex").slice(0, 12)}\u2026
1683
+ ` topic: ${TOPIC_PREFIX}${profile.league} \u2192 ${session.topic.toString("hex").slice(0, 12)}\u2026${topics.length > 1 ? ` (+${topics.length - 1} more)` : ""}
1644
1684
  `
1645
1685
  );
1646
1686
  process2.stdout.write(" type to chat \xB7 /next \xB7 /open <handle> \xB7 /quit\n");
@@ -1688,8 +1728,8 @@ var HELP = `vibedating ${VERSION} \u2014 dating by tokens (local-first)
1688
1728
  Usage:
1689
1729
  vibedating connect Read your usage, compute + print your league
1690
1730
  vibedating matches [--live] List candidates in your league (live peers if any)
1691
- vibedating discover [--live] Find live same-league peers over the DHT (opt-in)
1692
- vibedating live [--dating] Live chat with same-league peers (omegle /next, or --dating pick)
1731
+ 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)
1693
1733
  vibedating open [--port N] Serve the local web app (default: random port)
1694
1734
  + live A/V video with connected same-league peers
1695
1735
  vibedating mcp Run the stdio MCP server (profile, matches)
@@ -1701,6 +1741,10 @@ Privacy:
1701
1741
  bucket is ever shared. Live discovery (off by default) shares ONLY your
1702
1742
  handle + league + harness with same-league peers \u2014 opt in with --live.
1703
1743
 
1744
+ Matching:
1745
+ discover/live match your league + adjacent (\xB11) tiers by default, so thin
1746
+ leagues and cross-league friends still connect. --any matches every league.
1747
+
1704
1748
  Env:
1705
1749
  VIBEDATING_TOKENS=<n> Self-report a token count (e.g. 23400000 or 12M)
1706
1750
  VIBEDATING_HARNESS=<h> Harness id (claude-code, codex, \u2026)
@@ -1722,11 +1766,11 @@ async function main(argv) {
1722
1766
  case "matches":
1723
1767
  return cmdMatches(parsed.live);
1724
1768
  case "discover":
1725
- return cmdDiscover(parsed.live);
1769
+ return cmdDiscover(parsed.live, parsed.any);
1726
1770
  case "open":
1727
1771
  return cmdOpen(parsed.port);
1728
1772
  case "live":
1729
- return cmdLive(parsed.dating);
1773
+ return cmdLive(parsed.dating, parsed.any);
1730
1774
  case "mcp":
1731
1775
  await runMcp();
1732
1776
  return 0;
package/dist/index.d.ts CHANGED
@@ -142,9 +142,24 @@ interface DiscoveryOptions {
142
142
  readonly hello: PeerHello;
143
143
  /**
144
144
  * Override the joined topic (tests pass a random one on an isolated DHT).
145
- * Defaults to {@link leagueTopic}`(hello.league)`.
145
+ * Defaults to {@link leagueTopic}`(hello.league)`. Ignored when {@link topics}
146
+ * is set.
146
147
  */
147
148
  readonly topic?: Buffer;
149
+ /**
150
+ * ALL topics to join on the one swarm (e.g. your league + adjacent leagues),
151
+ * so thin pools and cross-league friends still connect. Every topic is
152
+ * joined, refreshed, and left on close. Defaults to `[topic]` — i.e. a single
153
+ * own-league topic (the legacy behavior).
154
+ */
155
+ readonly topics?: readonly Buffer[];
156
+ /**
157
+ * Predicate over an incoming peer's advertised league. Defaults to EXACT
158
+ * match against `hello.league` — the same privacy invariant as before. Widen
159
+ * it (e.g. ±1 adjacency via {@link leaguesWithin}) to accept cross-league
160
+ * peers that arrive on a shared topic.
161
+ */
162
+ readonly acceptLeague?: (peerLeague: string) => boolean;
148
163
  /** DHT bootstrap nodes; omit for the public DHT. Tests pass a local testnet. */
149
164
  readonly bootstrap?: ReadonlyArray<{
150
165
  readonly host: string;
@@ -165,15 +180,17 @@ interface DiscoveryOptions {
165
180
  readonly notify?: NotifySink;
166
181
  }
167
182
  interface DiscoverySession {
168
- /** The 32-byte topic actually joined. */
183
+ /** The primary (first) joined topic. See {@link topics} for the full set. */
169
184
  readonly topic: Buffer;
185
+ /** Every topic this session joined (primary first). */
186
+ readonly topics: readonly Buffer[];
170
187
  /** What we broadcast on every connection. */
171
188
  readonly hello: PeerHello;
172
189
  /** Live peer set, keyed by the remote's public key (hex). */
173
190
  readonly peers: ReadonlyMap<string, PeerHello>;
174
- /** Resolves when the first DHT announce/lookup round for the topic completes. */
191
+ /** Resolves when the first DHT announce/lookup round for every topic completes. */
175
192
  readonly ready: Promise<unknown>;
176
- /** Leave the topic and destroy the node. Idempotent. */
193
+ /** Leave every topic and destroy the node. Idempotent. */
177
194
  close(): Promise<void>;
178
195
  }
179
196
  /**
@@ -222,6 +239,29 @@ declare function league(totalTokens: number): {
222
239
  };
223
240
  /** Index of a league name in {@link LEAGUES}; {@link BELOW_LEAGUE} → -1. */
224
241
  declare function leagueIndex(name: string): number;
242
+ /**
243
+ * Every league name on the ladder: {@link BELOW_LEAGUE} first (it sits just
244
+ * below the 1M tier), then each {@link LEAGUES} name in ascending order.
245
+ * Pure; returns a fresh array each call.
246
+ */
247
+ declare function allLeagueNames(): string[];
248
+ /**
249
+ * League names whose index is within ±`width` of `name`'s index on the ladder,
250
+ * clamped to the array bounds. Ascending (index order), de-duplicated.
251
+ *
252
+ * Edge-case decisions:
253
+ * - **{@link BELOW_LEAGUE}** is treated as "index -1" — it sits just below the
254
+ * 1M tier, so `width` extends ONLY upward into the ladder
255
+ * (`leaguesWithin('below-1M', 1) → ['below-1M', '1M']`). This mirrors the
256
+ * existing `matches()` rule that below-1M is adjacent only to 1M.
257
+ * - **Unknown league names** (not in {@link LEAGUES}, not {@link BELOW_LEAGUE})
258
+ * return `[]` — we can't place them on the ladder, so they have no
259
+ * neighborhood.
260
+ * - **Negative width** is clamped to 0 (self only); non-integer width is
261
+ * truncated.
262
+ * - **Width larger than the ladder** is clamped to the bounds (whole ladder).
263
+ */
264
+ declare function leaguesWithin(name: string, width: number): string[];
225
265
  /** Env var that holds a self-reported total token count (e.g. `23400000`). */
226
266
  declare const TOKENS_ENV = "VIBEDATING_TOKENS";
227
267
  /**
@@ -277,4 +317,4 @@ declare const CANDIDATES: readonly Candidate[];
277
317
  */
278
318
  declare function matches(myLeague: string, candidates?: readonly Candidate[]): Candidate[];
279
319
 
280
- export { BELOW_LEAGUE, CANDIDATES, type Candidate, DEMO_TOTAL_TOKENS, type DiscoveryOptions, type DiscoverySession, LEAGUES, LIVE_NOTICE, type League, type PeerHello, type StoredPeer, TOKENS_ENV, TOPIC_PREFIX, league, leagueIndex, leagueTopic, loadPeers, matches, parseHandshake, parseTokensEnv, readUsage, recordPeer, serializeHandshake, startDiscovery, tryReadVerifiedUsage };
320
+ export { BELOW_LEAGUE, CANDIDATES, type Candidate, DEMO_TOTAL_TOKENS, type DiscoveryOptions, type DiscoverySession, LEAGUES, LIVE_NOTICE, type League, type PeerHello, type StoredPeer, TOKENS_ENV, TOPIC_PREFIX, allLeagueNames, league, leagueIndex, leagueTopic, leaguesWithin, loadPeers, matches, parseHandshake, parseTokensEnv, readUsage, recordPeer, serializeHandshake, startDiscovery, tryReadVerifiedUsage };
package/dist/index.js CHANGED
@@ -6,10 +6,12 @@ import {
6
6
  LIVE_NOTICE,
7
7
  TOKENS_ENV,
8
8
  TOPIC_PREFIX,
9
+ allLeagueNames,
9
10
  createConsentLedger,
10
11
  league,
11
12
  leagueIndex,
12
13
  leagueTopic,
14
+ leaguesWithin,
13
15
  loadPeers,
14
16
  matches,
15
17
  parseHandshake,
@@ -19,7 +21,7 @@ import {
19
21
  serializeHandshake,
20
22
  startDiscovery,
21
23
  tryReadVerifiedUsage
22
- } from "./chunk-6NWF2VD7.js";
24
+ } from "./chunk-VBLFAOV3.js";
23
25
  export {
24
26
  BELOW_LEAGUE,
25
27
  CANDIDATES,
@@ -28,10 +30,12 @@ export {
28
30
  LIVE_NOTICE,
29
31
  TOKENS_ENV,
30
32
  TOPIC_PREFIX,
33
+ allLeagueNames,
31
34
  createConsentLedger,
32
35
  league,
33
36
  leagueIndex,
34
37
  leagueTopic,
38
+ leaguesWithin,
35
39
  loadPeers,
36
40
  matches,
37
41
  parseHandshake,
package/dist/mcp.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  runMcp
3
- } from "./chunk-5ZJIPMU6.js";
4
- import "./chunk-6NWF2VD7.js";
3
+ } from "./chunk-DMA7MSN7.js";
4
+ import "./chunk-VBLFAOV3.js";
5
5
  export {
6
6
  runMcp
7
7
  };
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "vibedate",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
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",
7
7
  "author": "Pooria Arab",
8
8
  "bin": {
9
- "vibedating": "./dist/cli.js"
9
+ "vibedate": "./dist/cli.js",
10
+ "vibedating": "./dist/cli.js",
11
+ "vibedate-mcp": "./dist/mcp.js"
10
12
  },
11
13
  "main": "./dist/index.js",
12
14
  "types": "./dist/index.d.ts",