vibedate 0.1.0 → 0.2.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/index.d.ts CHANGED
@@ -1,6 +1,188 @@
1
- import { Harness, UsageSnapshot } from '@pooriaarab/vibe-core';
1
+ import { VibeEvent, Harness, UsageSnapshot } from '@pooriaarab/vibe-core';
2
2
  export { Harness, UsageSnapshot, createConsentLedger } from '@pooriaarab/vibe-core';
3
3
 
4
+ type Frame = {
5
+ t: 'hello';
6
+ handle: string;
7
+ league: string;
8
+ harness: string;
9
+ } | {
10
+ t: 'msg';
11
+ id: string;
12
+ text: string;
13
+ at: number;
14
+ } | {
15
+ t: 'typing';
16
+ } | {
17
+ t: 'bye';
18
+ } | {
19
+ t: 'media-start';
20
+ id: string;
21
+ mime: string;
22
+ size: number;
23
+ name: string;
24
+ } | {
25
+ t: 'media-chunk';
26
+ id: string;
27
+ seq: number;
28
+ b64: string;
29
+ } | {
30
+ t: 'media-end';
31
+ id: string;
32
+ } | {
33
+ t: 'rtc-offer';
34
+ sdp: string;
35
+ } | {
36
+ t: 'rtc-answer';
37
+ sdp: string;
38
+ } | {
39
+ t: 'rtc-ice';
40
+ candidate: string;
41
+ };
42
+ /** Convenience union of the three WebRTC signaling frame types (offer / answer
43
+ * / ice). Live A/V runs in the BROWSER via a native RTCPeerConnection; these
44
+ * frames only RELAY signaling over the P2P socket — no media bytes, no native
45
+ * WebRTC dependency in the CLI. */
46
+ type RtcFrame = Extract<Frame, {
47
+ t: `rtc-${string}`;
48
+ }>;
49
+
50
+ interface ReceivedMedia {
51
+ readonly mime: string;
52
+ readonly name: string;
53
+ /** Temp file path holding the reassembled bytes. */
54
+ readonly path: string;
55
+ readonly size: number;
56
+ }
57
+
58
+ interface PeerLink {
59
+ /** The validated identity of the remote peer (from the hello handshake). */
60
+ readonly hello: {
61
+ handle: string;
62
+ league: string;
63
+ harness: string;
64
+ };
65
+ /** Send a line of text as a `msg` frame. */
66
+ send(text: string): void;
67
+ /** Read a file from disk and send it as a chunked media transfer. */
68
+ sendMedia(filePath: string, opts?: {
69
+ mime?: string;
70
+ name?: string;
71
+ }): Promise<{
72
+ id: string;
73
+ size: number;
74
+ }>;
75
+ /** Relay one `rtc-*` signaling frame (offer / answer / ice) to the peer.
76
+ * Live media never touches this socket — only SDP / ICE strings do. */
77
+ sendSignal(frame: RtcFrame): void;
78
+ /** Register a callback for each incoming `msg` frame. */
79
+ onMessage(cb: (m: {
80
+ id: string;
81
+ text: string;
82
+ at: number;
83
+ }) => void): void;
84
+ /** Register a callback fired for each fully-reassembled incoming media file. */
85
+ onMedia(cb: (m: ReceivedMedia) => void): void;
86
+ /** Register a callback fired for each incoming `rtc-*` signaling frame. */
87
+ onSignal(cb: (f: RtcFrame) => void): void;
88
+ /** Register a callback fired once when the peer closes the link. */
89
+ onClose(cb: () => void): void;
90
+ /** Omegle "next": write a `bye` frame, then end the socket. */
91
+ close(): void;
92
+ }
93
+
94
+ /** Namespace prefix so vibedating topics never collide with other DHT traffic. */
95
+ declare const TOPIC_PREFIX = "vibedate:";
96
+ /**
97
+ * Derive the 32-byte DHT topic for a league bucket. Deterministic: everyone in
98
+ * the same league anywhere in the world hashes to the same topic, which is the
99
+ * entire discovery mechanism. Pure.
100
+ */
101
+ declare function leagueTopic(leagueName: string): Buffer;
102
+ /** The ONLY three fields that ever leave the machine over a peer connection. */
103
+ interface PeerHello {
104
+ readonly handle: string;
105
+ readonly league: string;
106
+ readonly harness: string;
107
+ }
108
+ /** One-line privacy notice printed before joining the swarm. */
109
+ declare const LIVE_NOTICE = "live discovery: sharing only your handle + league + harness (never raw usage) with same-league peers on the public DHT";
110
+ /**
111
+ * Serialize a hello to the single JSON line sent on connect. Built key-by-key
112
+ * from the allowlist — even if a caller sneaks extra properties onto the
113
+ * object, they cannot leak into the wire format.
114
+ */
115
+ declare function serializeHandshake(hello: PeerHello): string;
116
+ /**
117
+ * Parse one incoming handshake line. Returns `null` for anything malformed
118
+ * (bad JSON, non-object, missing/oversized handle or league). The result is
119
+ * constructed from an allowlist of keys, so any extra fields a peer sends —
120
+ * in particular any raw-usage field — are ignored and never retained.
121
+ */
122
+ declare function parseHandshake(raw: string | Buffer): PeerHello | null;
123
+ /** A peer we've shaken hands with, persisted locally. */
124
+ interface StoredPeer extends PeerHello {
125
+ readonly firstSeenAt: string;
126
+ readonly lastSeenAt: string;
127
+ }
128
+ /** Load persisted live peers, or `[]` if none/corrupt. Local-only data. */
129
+ declare function loadPeers(dir?: string): StoredPeer[];
130
+ /**
131
+ * Record a successfully handshaken peer, keyed by handle (a peer may reconnect
132
+ * from a different key). Returns whether this handle is NEW (first time seen).
133
+ */
134
+ declare function recordPeer(hello: PeerHello, dir?: string, now?: Date): {
135
+ peer: StoredPeer;
136
+ isNew: boolean;
137
+ };
138
+ /** Injection point for the match notification (defaults to vibe-core notify). */
139
+ type NotifySink = (event: VibeEvent) => void;
140
+ interface DiscoveryOptions {
141
+ /** What we broadcast. Must already be consent-gated by the caller. */
142
+ readonly hello: PeerHello;
143
+ /**
144
+ * Override the joined topic (tests pass a random one on an isolated DHT).
145
+ * Defaults to {@link leagueTopic}`(hello.league)`.
146
+ */
147
+ readonly topic?: Buffer;
148
+ /** DHT bootstrap nodes; omit for the public DHT. Tests pass a local testnet. */
149
+ readonly bootstrap?: ReadonlyArray<{
150
+ readonly host: string;
151
+ readonly port: number;
152
+ }>;
153
+ /** Where peers.json lives. Defaults to ~/.vibedating. */
154
+ readonly stateDir?: string;
155
+ /** Called after each accepted handshake; `isNew` = first time this handle is seen. */
156
+ readonly onPeer?: (peer: PeerHello, isNew: boolean) => void;
157
+ /**
158
+ * Called once per connection with a live {@link PeerLink} over the same socket
159
+ * (the hello was frame #1; subsequent frames flow to the link). Omit for the
160
+ * plain `discover` behavior (no live chat). Existing discovery behavior is
161
+ * unchanged when this is absent.
162
+ */
163
+ readonly onLink?: (link: PeerLink) => void;
164
+ /** Match-notification sink (tests capture with a fake). Best-effort. */
165
+ readonly notify?: NotifySink;
166
+ }
167
+ interface DiscoverySession {
168
+ /** The 32-byte topic actually joined. */
169
+ readonly topic: Buffer;
170
+ /** What we broadcast on every connection. */
171
+ readonly hello: PeerHello;
172
+ /** Live peer set, keyed by the remote's public key (hex). */
173
+ readonly peers: ReadonlyMap<string, PeerHello>;
174
+ /** Resolves when the first DHT announce/lookup round for the topic completes. */
175
+ readonly ready: Promise<unknown>;
176
+ /** Leave the topic and destroy the node. Idempotent. */
177
+ close(): Promise<void>;
178
+ }
179
+ /**
180
+ * Join the swarm on the league topic and handshake with every peer that
181
+ * connects. CONSENT GATE LIVES WITH THE CALLER — never call this without the
182
+ * `share:live` grant (or an explicit `--live` opt-in in the same breath).
183
+ */
184
+ declare function startDiscovery(opts: DiscoveryOptions): Promise<DiscoverySession>;
185
+
4
186
  /**
5
187
  * vibedating — dating by token usage.
6
188
  *
@@ -95,4 +277,4 @@ declare const CANDIDATES: readonly Candidate[];
95
277
  */
96
278
  declare function matches(myLeague: string, candidates?: readonly Candidate[]): Candidate[];
97
279
 
98
- export { BELOW_LEAGUE, CANDIDATES, type Candidate, DEMO_TOTAL_TOKENS, LEAGUES, type League, TOKENS_ENV, league, leagueIndex, matches, parseTokensEnv, readUsage, tryReadVerifiedUsage };
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 };
package/dist/index.js CHANGED
@@ -3,26 +3,42 @@ import {
3
3
  CANDIDATES,
4
4
  DEMO_TOTAL_TOKENS,
5
5
  LEAGUES,
6
+ LIVE_NOTICE,
6
7
  TOKENS_ENV,
8
+ TOPIC_PREFIX,
7
9
  createConsentLedger,
8
10
  league,
9
11
  leagueIndex,
12
+ leagueTopic,
13
+ loadPeers,
10
14
  matches,
15
+ parseHandshake,
11
16
  parseTokensEnv,
12
17
  readUsage,
18
+ recordPeer,
19
+ serializeHandshake,
20
+ startDiscovery,
13
21
  tryReadVerifiedUsage
14
- } from "./chunk-AU7FN2LY.js";
22
+ } from "./chunk-6NWF2VD7.js";
15
23
  export {
16
24
  BELOW_LEAGUE,
17
25
  CANDIDATES,
18
26
  DEMO_TOTAL_TOKENS,
19
27
  LEAGUES,
28
+ LIVE_NOTICE,
20
29
  TOKENS_ENV,
30
+ TOPIC_PREFIX,
21
31
  createConsentLedger,
22
32
  league,
23
33
  leagueIndex,
34
+ leagueTopic,
35
+ loadPeers,
24
36
  matches,
37
+ parseHandshake,
25
38
  parseTokensEnv,
26
39
  readUsage,
40
+ recordPeer,
41
+ serializeHandshake,
42
+ startDiscovery,
27
43
  tryReadVerifiedUsage
28
44
  };
package/dist/mcp.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  runMcp
3
- } from "./chunk-64ME4THO.js";
4
- import "./chunk-AU7FN2LY.js";
3
+ } from "./chunk-5ZJIPMU6.js";
4
+ import "./chunk-6NWF2VD7.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.1.0",
3
+ "version": "0.2.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",
@@ -26,11 +26,13 @@
26
26
  "test:watch": "vitest"
27
27
  },
28
28
  "dependencies": {
29
- "@pooriaarab/vibe-core": "^0.1.0",
30
- "@modelcontextprotocol/sdk": "^1.0.0"
29
+ "@modelcontextprotocol/sdk": "^1.0.0",
30
+ "@pooriaarab/vibe-core": "^0.2.0",
31
+ "hyperswarm": "^4.17.0"
31
32
  },
32
33
  "devDependencies": {
33
34
  "@types/node": "^20.11.0",
35
+ "hyperdht": "^6.33.0",
34
36
  "tsup": "^8.0.0",
35
37
  "typescript": "^5.4.0",
36
38
  "vitest": "^1.6.0"
@@ -1,133 +0,0 @@
1
- // src/index.ts
2
- import { createConsentLedger } from "@pooriaarab/vibe-core";
3
- var LEAGUES = [
4
- { name: "1M", min: 1e6, max: 4999999 },
5
- { name: "5M", min: 5e6, max: 9999999 },
6
- { name: "10M", min: 1e7, max: 99999999 },
7
- { name: "100M", min: 1e8, max: 999999999 },
8
- { name: "1B+", min: 1e9, max: Number.POSITIVE_INFINITY }
9
- ];
10
- var BELOW_LEAGUE = "below-1M";
11
- function league(totalTokens) {
12
- const n = Math.max(0, Math.floor(totalTokens));
13
- for (const l of LEAGUES) {
14
- if (n >= l.min && n <= l.max) {
15
- return { name: l.name, min: l.min };
16
- }
17
- }
18
- return { name: BELOW_LEAGUE, min: 0 };
19
- }
20
- function leagueIndex(name) {
21
- if (name === BELOW_LEAGUE) return -1;
22
- return LEAGUES.findIndex((l) => l.name === name);
23
- }
24
- var TOKENS_ENV = "VIBEDATING_TOKENS";
25
- var DEMO_TOTAL_TOKENS = 234e5;
26
- var MS_PER_DAY = 864e5;
27
- async function readUsage(harness = "claude-code") {
28
- const verified = await tryReadVerifiedUsage(harness);
29
- if (verified) return verified;
30
- const injected = parseTokensEnv(process.env[TOKENS_ENV]);
31
- const totalTokens = injected ?? DEMO_TOTAL_TOKENS;
32
- const now = /* @__PURE__ */ new Date();
33
- return {
34
- harness,
35
- totalTokens,
36
- verified: false,
37
- windowStart: new Date(now.getTime() - 30 * MS_PER_DAY).toISOString(),
38
- windowEnd: now.toISOString()
39
- };
40
- }
41
- async function tryReadVerifiedUsage(_harness) {
42
- return null;
43
- }
44
- var TOKEN_MULT = {
45
- "": 1,
46
- k: 1e3,
47
- K: 1e3,
48
- m: 1e6,
49
- M: 1e6,
50
- b: 1e9,
51
- B: 1e9
52
- };
53
- function parseTokensEnv(raw) {
54
- if (raw === void 0) return void 0;
55
- const trimmed = raw.trim();
56
- if (trimmed === "") return void 0;
57
- const match = /^([0-9]*\.?[0-9]+)\s*([kKmMbB]?)$/.exec(trimmed);
58
- if (!match) return void 0;
59
- const num = Number(match[1]);
60
- if (!Number.isFinite(num) || num < 0) return void 0;
61
- const mult = TOKEN_MULT[match[2] ?? ""] ?? 1;
62
- return Math.floor(num * mult);
63
- }
64
- var CANDIDATES = [
65
- {
66
- handle: "@merge_conflict_therapist",
67
- league: "10M",
68
- bio: ["Resolves conflicts for a living \u2014 code and otherwise.", "Currently: 47 tabs open, 3 are Stack Overflow."]
69
- },
70
- {
71
- handle: "@rebase_romantic",
72
- league: "5M",
73
- bio: ["Rewrites history for a living, git and otherwise.", "Looking for someone who squashes commits and grudges."]
74
- },
75
- {
76
- handle: "@0xInsomniac",
77
- league: "1B+",
78
- bio: ["Token count is classified. Ask my therapist.", "Has never once respected a rate limit."]
79
- },
80
- {
81
- handle: "@yolo_to_main",
82
- league: "1M",
83
- bio: ["No branches, no regrets, no CI.", "A green checkmark is a state of mind."]
84
- },
85
- {
86
- handle: "@async_awaits_you",
87
- league: "10M",
88
- bio: ["Promises kept, unlike my sleep schedule.", "DMs are non-blocking. Replies eventually resolve."]
89
- },
90
- {
91
- handle: "@nullish_and_void",
92
- league: "5M",
93
- bio: ["Coalescing since 2019.", "My love language is optional chaining."]
94
- },
95
- {
96
- handle: "@ctrl_z_daddy",
97
- league: "100M",
98
- bio: ["Undo is my safe word.", "Refactors everything, including this bio, twice."]
99
- },
100
- {
101
- handle: "@segfault_sonnet",
102
- league: "1M",
103
- bio: ["Writes poetry in stack traces.", "Core dumped. Heart, mostly, open."]
104
- },
105
- {
106
- handle: "@the_lint_whisperer",
107
- league: "10M",
108
- bio: ["Zero warnings, zero regrets, zero chill.", "Will fix your semicolons without being asked."]
109
- }
110
- ];
111
- function matches(myLeague, candidates = CANDIDATES) {
112
- const myIdx = leagueIndex(myLeague);
113
- return candidates.filter((c) => {
114
- const idx = leagueIndex(c.league);
115
- if (idx < 0) return false;
116
- return Math.abs(idx - myIdx) <= 1;
117
- });
118
- }
119
-
120
- export {
121
- LEAGUES,
122
- BELOW_LEAGUE,
123
- league,
124
- leagueIndex,
125
- TOKENS_ENV,
126
- DEMO_TOTAL_TOKENS,
127
- readUsage,
128
- tryReadVerifiedUsage,
129
- parseTokensEnv,
130
- CANDIDATES,
131
- matches,
132
- createConsentLedger
133
- };