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/README.md +14 -1
- package/dist/{chunk-ZB56ZBAR.js → chunk-JIBC3OHV.js} +1 -1
- package/dist/{chunk-43IYNWES.js → chunk-VZXPYU2C.js} +259 -1
- package/dist/cli.d.ts +23 -2
- package/dist/cli.js +631 -22
- package/dist/index.d.ts +136 -1
- package/dist/index.js +15 -1
- package/dist/mcp.js +2 -2
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -9,6 +9,12 @@ Part of the **Vibe Suite** — companion tools for agentic coding CLIs. Ships as
|
|
|
9
9
|
**CLI + npm package + MCP server**, plus a local web app. Built on
|
|
10
10
|
[`@pooriaarab/vibe-core`](https://www.npmjs.com/package/@pooriaarab/vibe-core).
|
|
11
11
|
|
|
12
|
+
## Demo
|
|
13
|
+
|
|
14
|
+
[▶ Watch the launch video](branding/launch-video.mp4) — she matched with the man of her dreams. He was coding in a basement.
|
|
15
|
+
|
|
16
|
+
https://github.com/pooriaarab/vibedating/raw/main/branding/launch-video.mp4
|
|
17
|
+
|
|
12
18
|
> **Local-first.** Raw token usage is read and stored on your own machine and
|
|
13
19
|
> **never leaves it.** Only the coarse league *bucket* is ever shared — never the
|
|
14
20
|
> raw number, never per-project breakdowns. Live matching is peer-to-peer over a
|
|
@@ -62,8 +68,15 @@ VIBEDATING_TOKENS=23400000 vibedating connect # also accepts 12M / 1.2B / 500k
|
|
|
62
68
|
|
|
63
69
|
```
|
|
64
70
|
vibedating connect Read usage, compute + print your league
|
|
65
|
-
|
|
71
|
+
(first run auto-assigns a memetic handle — never @you)
|
|
72
|
+
vibedating matches [--live] List candidates in your league (live peers if any,
|
|
73
|
+
with last-message time)
|
|
66
74
|
vibedating discover [--live] Find live same-league peers over the DHT (opt-in)
|
|
75
|
+
vibedating live [--keep-alive] Live text chat (--keep-alive / non-TTY survives stdin EOF)
|
|
76
|
+
vibedating handle [@name] Print or set your handle
|
|
77
|
+
vibedating daemon [start|stop|status|install|uninstall]
|
|
78
|
+
Notify-only background daemon — alerts on NEW matches,
|
|
79
|
+
never opens chat/video; install = run on login (launchd/systemd)
|
|
67
80
|
vibedating open [--port N] Serve the local web app (default: random free port)
|
|
68
81
|
vibedating mcp Run the stdio MCP server (tools: profile, matches)
|
|
69
82
|
vibedating --version
|
|
@@ -388,6 +388,44 @@ function classifyHelloIdentity(hello) {
|
|
|
388
388
|
if (hello.nonce === void 0 || hello.sig === void 0) return "drop";
|
|
389
389
|
return verifyHelloClaims(hello, { pubkey: hello.pubkey, nonce: hello.nonce, sig: hello.sig }) ? "verified" : "drop";
|
|
390
390
|
}
|
|
391
|
+
var NOSTR_FILE = "nostr.json";
|
|
392
|
+
function nostrPath(dir) {
|
|
393
|
+
return path2.join(dir, NOSTR_FILE);
|
|
394
|
+
}
|
|
395
|
+
function isStoredNostrKey(data) {
|
|
396
|
+
if (typeof data !== "object" || data === null) return false;
|
|
397
|
+
const r = data;
|
|
398
|
+
return typeof r["sk"] === "string" && /^[0-9a-f]{64}$/.test(r["sk"]) && typeof r["pubkey"] === "string" && /^[0-9a-f]{64}$/.test(r["pubkey"]) && typeof r["createdAt"] === "string";
|
|
399
|
+
}
|
|
400
|
+
async function loadOrCreateNostrKey(dir = defaultStateDir()) {
|
|
401
|
+
try {
|
|
402
|
+
const raw = readFileSync2(nostrPath(dir), "utf8");
|
|
403
|
+
const data = JSON.parse(raw);
|
|
404
|
+
if (isStoredNostrKey(data)) {
|
|
405
|
+
try {
|
|
406
|
+
chmodSync(nostrPath(dir), 384);
|
|
407
|
+
} catch {
|
|
408
|
+
}
|
|
409
|
+
return { sk: Buffer.from(data.sk, "hex"), pubkey: data.pubkey };
|
|
410
|
+
}
|
|
411
|
+
} catch {
|
|
412
|
+
}
|
|
413
|
+
const { generateSecretKey, getPublicKey } = await import("nostr-tools");
|
|
414
|
+
const sk = generateSecretKey();
|
|
415
|
+
const pubkey = getPublicKey(sk);
|
|
416
|
+
const skHex = Buffer.from(sk).toString("hex");
|
|
417
|
+
mkdirSync2(dir, { recursive: true });
|
|
418
|
+
writeFileSync2(
|
|
419
|
+
nostrPath(dir),
|
|
420
|
+
JSON.stringify({ sk: skHex, pubkey, createdAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n",
|
|
421
|
+
{ encoding: "utf8", mode: 384 }
|
|
422
|
+
);
|
|
423
|
+
try {
|
|
424
|
+
chmodSync(nostrPath(dir), 384);
|
|
425
|
+
} catch {
|
|
426
|
+
}
|
|
427
|
+
return { sk, pubkey };
|
|
428
|
+
}
|
|
391
429
|
|
|
392
430
|
// src/link.ts
|
|
393
431
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
@@ -656,6 +694,14 @@ function createPeerLink(socket, hello, initialBuffer = "", linkOpts = {}) {
|
|
|
656
694
|
};
|
|
657
695
|
}
|
|
658
696
|
|
|
697
|
+
// src/untrusted.ts
|
|
698
|
+
var MAX_DISPLAY_TEXT_LEN = MAX_TEXT_LEN;
|
|
699
|
+
var UNSAFE_DISPLAY_CHARS = /[\u0000-\u0008\u000B-\u001F\u007F-\u009F\u200E\u200F\u202A-\u202E\u2066-\u2069\uFEFF]/g;
|
|
700
|
+
function sanitizePeerText(text, maxLen = MAX_DISPLAY_TEXT_LEN) {
|
|
701
|
+
const cleaned = text.replace(UNSAFE_DISPLAY_CHARS, "");
|
|
702
|
+
return cleaned.length > maxLen ? cleaned.slice(0, maxLen) : cleaned;
|
|
703
|
+
}
|
|
704
|
+
|
|
659
705
|
// src/p2p.ts
|
|
660
706
|
var TOPIC_PREFIX = "vibedate:";
|
|
661
707
|
function leagueTopic(leagueName) {
|
|
@@ -838,7 +884,9 @@ async function startDiscovery(opts) {
|
|
|
838
884
|
try {
|
|
839
885
|
notify(
|
|
840
886
|
makeEvent("match", hello.harness, process.cwd(), {
|
|
841
|
-
|
|
887
|
+
// AEGIS-lite: the handle is untrusted wire data — sanitized for
|
|
888
|
+
// display (the structured `handle` field below stays verbatim).
|
|
889
|
+
summary: `matched with ${sanitizePeerText(peer.handle)} - LIVE SAME LEAGUE`,
|
|
842
890
|
handle: peer.handle,
|
|
843
891
|
league: peer.league,
|
|
844
892
|
harness: peer.harness
|
|
@@ -899,6 +947,204 @@ async function startDiscovery(opts) {
|
|
|
899
947
|
};
|
|
900
948
|
}
|
|
901
949
|
|
|
950
|
+
// src/relay.ts
|
|
951
|
+
import { createHash as createHash2 } from "crypto";
|
|
952
|
+
var DEFAULT_RELAYS = [
|
|
953
|
+
"wss://relay.damus.io",
|
|
954
|
+
"wss://nostr.mom",
|
|
955
|
+
"wss://relay.snort.social"
|
|
956
|
+
];
|
|
957
|
+
var VIBEDATE_MESSAGE_KIND = 4;
|
|
958
|
+
var VIBEDATE_PRESENCE_KIND = 30078;
|
|
959
|
+
var PRESENCE_D_TAG = "vibedating";
|
|
960
|
+
var PRESENCE_MARKER = JSON.stringify({ app: "vibedating", v: 1 });
|
|
961
|
+
var ED25519_TAG = "ed25519";
|
|
962
|
+
function conversationTag(myEd25519Hex, peerEd25519Hex) {
|
|
963
|
+
const lo = myEd25519Hex < peerEd25519Hex ? myEd25519Hex : peerEd25519Hex;
|
|
964
|
+
const hi = myEd25519Hex < peerEd25519Hex ? peerEd25519Hex : myEd25519Hex;
|
|
965
|
+
return createHash2("sha256").update(`vibedate:nostr:${lo}:${hi}`, "utf8").digest("hex");
|
|
966
|
+
}
|
|
967
|
+
var nostrPromise;
|
|
968
|
+
function nostr() {
|
|
969
|
+
if (!nostrPromise) nostrPromise = import("nostr-tools");
|
|
970
|
+
return nostrPromise;
|
|
971
|
+
}
|
|
972
|
+
function tagValue(event, name) {
|
|
973
|
+
for (const tag of event.tags) {
|
|
974
|
+
if (tag[0] === name && typeof tag[1] === "string") return tag[1];
|
|
975
|
+
}
|
|
976
|
+
return void 0;
|
|
977
|
+
}
|
|
978
|
+
async function createNostrRelayLink(opts) {
|
|
979
|
+
const { finalizeEvent, nip04 } = await nostr();
|
|
980
|
+
const { myNostr, myEd25519Hex, peerEd25519Hex, hello, transport } = opts;
|
|
981
|
+
const convTag = conversationTag(myEd25519Hex, peerEd25519Hex);
|
|
982
|
+
const messageCbs = /* @__PURE__ */ new Set();
|
|
983
|
+
const closeCbs = /* @__PURE__ */ new Set();
|
|
984
|
+
const mediaCbs = /* @__PURE__ */ new Set();
|
|
985
|
+
const signalCbs = /* @__PURE__ */ new Set();
|
|
986
|
+
let closed = false;
|
|
987
|
+
let peerNostrPubkey;
|
|
988
|
+
const pending = [];
|
|
989
|
+
const publish = (template) => {
|
|
990
|
+
let event;
|
|
991
|
+
try {
|
|
992
|
+
event = finalizeEvent(
|
|
993
|
+
{ ...template, created_at: Math.floor(Date.now() / 1e3) },
|
|
994
|
+
myNostr.sk
|
|
995
|
+
);
|
|
996
|
+
} catch {
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
try {
|
|
1000
|
+
transport.publish(event);
|
|
1001
|
+
} catch {
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
const publishPresence = () => {
|
|
1005
|
+
publish({
|
|
1006
|
+
kind: VIBEDATE_PRESENCE_KIND,
|
|
1007
|
+
content: PRESENCE_MARKER,
|
|
1008
|
+
tags: [
|
|
1009
|
+
["d", PRESENCE_D_TAG],
|
|
1010
|
+
["t", convTag],
|
|
1011
|
+
[ED25519_TAG, myEd25519Hex]
|
|
1012
|
+
]
|
|
1013
|
+
});
|
|
1014
|
+
};
|
|
1015
|
+
const sendEncrypted = (text, recipientPubkey) => {
|
|
1016
|
+
let ciphertext;
|
|
1017
|
+
try {
|
|
1018
|
+
ciphertext = nip04.encrypt(myNostr.sk, recipientPubkey, text);
|
|
1019
|
+
} catch {
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
publish({
|
|
1023
|
+
kind: VIBEDATE_MESSAGE_KIND,
|
|
1024
|
+
content: ciphertext,
|
|
1025
|
+
tags: [
|
|
1026
|
+
["t", convTag],
|
|
1027
|
+
["p", recipientPubkey],
|
|
1028
|
+
[ED25519_TAG, myEd25519Hex]
|
|
1029
|
+
]
|
|
1030
|
+
});
|
|
1031
|
+
};
|
|
1032
|
+
const flushPending = () => {
|
|
1033
|
+
if (peerNostrPubkey === void 0) return;
|
|
1034
|
+
while (pending.length > 0) {
|
|
1035
|
+
const text = pending.shift();
|
|
1036
|
+
if (text !== void 0) sendEncrypted(text, peerNostrPubkey);
|
|
1037
|
+
}
|
|
1038
|
+
};
|
|
1039
|
+
const unsubscribe = transport.subscribe({ "#t": [convTag] }, (event) => {
|
|
1040
|
+
if (closed) return;
|
|
1041
|
+
if (event.pubkey === myNostr.pubkey) return;
|
|
1042
|
+
if (event.kind === VIBEDATE_PRESENCE_KIND) {
|
|
1043
|
+
const claimedEd = tagValue(event, ED25519_TAG);
|
|
1044
|
+
if (claimedEd !== peerEd25519Hex) return;
|
|
1045
|
+
peerNostrPubkey = event.pubkey;
|
|
1046
|
+
flushPending();
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
if (event.kind === VIBEDATE_MESSAGE_KIND) {
|
|
1050
|
+
let plaintext;
|
|
1051
|
+
try {
|
|
1052
|
+
const sender = peerNostrPubkey ?? event.pubkey;
|
|
1053
|
+
plaintext = nip04.decrypt(myNostr.sk, sender, event.content);
|
|
1054
|
+
} catch {
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
peerNostrPubkey = event.pubkey;
|
|
1058
|
+
const msg = { id: event.id, text: plaintext, at: event.created_at * 1e3 };
|
|
1059
|
+
for (const cb of messageCbs) cb(msg);
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
publishPresence();
|
|
1064
|
+
return {
|
|
1065
|
+
hello,
|
|
1066
|
+
send(text) {
|
|
1067
|
+
if (closed) return;
|
|
1068
|
+
if (peerNostrPubkey === void 0) {
|
|
1069
|
+
pending.push(text);
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
sendEncrypted(text, peerNostrPubkey);
|
|
1073
|
+
},
|
|
1074
|
+
// ponytail: chunked media over the relay (mirror link.ts media frames as
|
|
1075
|
+
// kind-4 payloads). For v0 a relay link carries text only.
|
|
1076
|
+
async sendMedia() {
|
|
1077
|
+
return { id: "", size: 0 };
|
|
1078
|
+
},
|
|
1079
|
+
// ponytail: relay WebRTC signaling (rtc-offer/answer/ice as kind-4 payloads)
|
|
1080
|
+
// so A/V could also fall back through the relay. v0: text only.
|
|
1081
|
+
sendSignal() {
|
|
1082
|
+
},
|
|
1083
|
+
onMessage(cb) {
|
|
1084
|
+
messageCbs.add(cb);
|
|
1085
|
+
},
|
|
1086
|
+
onMedia(cb) {
|
|
1087
|
+
mediaCbs.add(cb);
|
|
1088
|
+
},
|
|
1089
|
+
onSignal(cb) {
|
|
1090
|
+
signalCbs.add(cb);
|
|
1091
|
+
},
|
|
1092
|
+
onClose(cb) {
|
|
1093
|
+
closeCbs.add(cb);
|
|
1094
|
+
},
|
|
1095
|
+
close() {
|
|
1096
|
+
if (closed) return;
|
|
1097
|
+
closed = true;
|
|
1098
|
+
try {
|
|
1099
|
+
unsubscribe();
|
|
1100
|
+
} catch {
|
|
1101
|
+
}
|
|
1102
|
+
try {
|
|
1103
|
+
transport.close();
|
|
1104
|
+
} catch {
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
async function createNostrPoolTransport(urls = DEFAULT_RELAYS) {
|
|
1110
|
+
const { SimplePool } = await nostr();
|
|
1111
|
+
const pool = new SimplePool();
|
|
1112
|
+
const relays = [...urls];
|
|
1113
|
+
return {
|
|
1114
|
+
publish(event) {
|
|
1115
|
+
try {
|
|
1116
|
+
const results = pool.publish(relays, event);
|
|
1117
|
+
Promise.allSettled(results).catch(() => {
|
|
1118
|
+
});
|
|
1119
|
+
} catch {
|
|
1120
|
+
}
|
|
1121
|
+
},
|
|
1122
|
+
subscribe(filter, onEvent) {
|
|
1123
|
+
const closer = pool.subscribeMany(
|
|
1124
|
+
relays,
|
|
1125
|
+
filter,
|
|
1126
|
+
{ onevent: (e) => onEvent(e) }
|
|
1127
|
+
);
|
|
1128
|
+
return () => {
|
|
1129
|
+
try {
|
|
1130
|
+
closer.close();
|
|
1131
|
+
} catch {
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
},
|
|
1135
|
+
close() {
|
|
1136
|
+
try {
|
|
1137
|
+
pool.close(relays);
|
|
1138
|
+
} catch {
|
|
1139
|
+
}
|
|
1140
|
+
try {
|
|
1141
|
+
pool.destroy();
|
|
1142
|
+
} catch {
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
|
|
902
1148
|
// src/index.ts
|
|
903
1149
|
var LEAGUES = [
|
|
904
1150
|
{ name: "1M", min: 1e6, max: 4999999 },
|
|
@@ -1036,12 +1282,16 @@ function matches(myLeague, candidates = CANDIDATES) {
|
|
|
1036
1282
|
|
|
1037
1283
|
export {
|
|
1038
1284
|
parseFrame,
|
|
1285
|
+
defaultStateDir,
|
|
1039
1286
|
connectProfile,
|
|
1040
1287
|
loadProfile,
|
|
1041
1288
|
grantLiveConsent,
|
|
1042
1289
|
canShareLive,
|
|
1290
|
+
DEFAULT_HANDLE,
|
|
1291
|
+
MAX_HANDLE_LEN,
|
|
1043
1292
|
sameHandle,
|
|
1044
1293
|
normalizeHandle,
|
|
1294
|
+
loadHandle,
|
|
1045
1295
|
saveHandle,
|
|
1046
1296
|
resolveHandle,
|
|
1047
1297
|
loadBlocklist,
|
|
@@ -1053,6 +1303,8 @@ export {
|
|
|
1053
1303
|
signHelloClaims,
|
|
1054
1304
|
verifyHelloClaims,
|
|
1055
1305
|
classifyHelloIdentity,
|
|
1306
|
+
loadOrCreateNostrKey,
|
|
1307
|
+
sanitizePeerText,
|
|
1056
1308
|
TOPIC_PREFIX,
|
|
1057
1309
|
leagueTopic,
|
|
1058
1310
|
LIVE_NOTICE,
|
|
@@ -1062,6 +1314,12 @@ export {
|
|
|
1062
1314
|
recordPeer,
|
|
1063
1315
|
recordPeerMessage,
|
|
1064
1316
|
startDiscovery,
|
|
1317
|
+
DEFAULT_RELAYS,
|
|
1318
|
+
VIBEDATE_MESSAGE_KIND,
|
|
1319
|
+
VIBEDATE_PRESENCE_KIND,
|
|
1320
|
+
conversationTag,
|
|
1321
|
+
createNostrRelayLink,
|
|
1322
|
+
createNostrPoolTransport,
|
|
1065
1323
|
LEAGUES,
|
|
1066
1324
|
BELOW_LEAGUE,
|
|
1067
1325
|
league,
|
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' | 'find' | 'handle' | 'block' | 'unblock' | 'blocklist' | 'mcp' | 'help' | 'version' | null;
|
|
3
|
+
type Command = 'connect' | 'matches' | 'discover' | 'open' | 'live' | 'find' | 'handle' | 'block' | 'unblock' | 'blocklist' | 'daemon' | '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". */
|
|
@@ -15,11 +15,32 @@ interface ParsedArgs {
|
|
|
15
15
|
readonly arg: string | undefined;
|
|
16
16
|
/** `live --to <@handle>`: targeted match — auto-open that specific peer. */
|
|
17
17
|
readonly to: string | undefined;
|
|
18
|
+
/** `live --keep-alive`: stay in the swarm after stdin EOF. Default false. */
|
|
19
|
+
readonly keepAlive: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* `live --via-relay` / `discover --via-relay`: route through public Nostr relays
|
|
22
|
+
* (NIP-04 e2e) when direct hyperdht hole-punching fails. v0 explicit opt-in;
|
|
23
|
+
* auto-fallback-on-timeout is a follow-up. Default false.
|
|
24
|
+
*/
|
|
25
|
+
readonly viaRelay: boolean;
|
|
18
26
|
}
|
|
19
27
|
/**
|
|
20
28
|
* Parse argv (the slice AFTER the program name) into a command + options.
|
|
21
29
|
* Pure: no IO, no process access — trivially unit-testable.
|
|
22
30
|
*/
|
|
23
31
|
declare function parseArgs(argv: readonly string[]): ParsedArgs;
|
|
32
|
+
/**
|
|
33
|
+
* Compact relative time for local lists: an ISO timestamp → "just now" /
|
|
34
|
+
* "5m ago" / "3h ago" / "2d ago". Unparseable input → "unknown". Pure.
|
|
35
|
+
*/
|
|
36
|
+
declare function formatAgo(iso: string, now?: Date): string;
|
|
37
|
+
/**
|
|
38
|
+
* Whether `live` should stay in the swarm after stdin closes. An explicit
|
|
39
|
+
* `--keep-alive` always keeps it; otherwise AUTO-detect: a non-TTY stdin
|
|
40
|
+
* (piped, `</dev/null`, backgrounded) hits EOF immediately, and exiting there
|
|
41
|
+
* would kill an unattended session — so it stays up until SIGINT/SIGTERM.
|
|
42
|
+
* Interactive TTY behavior is unchanged: Ctrl+D / EOF still exits.
|
|
43
|
+
*/
|
|
44
|
+
declare function shouldKeepAlive(flag: boolean, stdinIsTTY: boolean | undefined): boolean;
|
|
24
45
|
|
|
25
|
-
export { type Command, type ParsedArgs, parseArgs };
|
|
46
|
+
export { type Command, type ParsedArgs, formatAgo, parseArgs, shouldKeepAlive };
|