vibedate 0.6.0 → 0.7.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/{chunk-VZXPYU2C.js → chunk-HDHJLZZG.js} +100 -0
- package/dist/{chunk-JIBC3OHV.js → chunk-ZD6JBWEW.js} +1 -1
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +370 -28
- package/dist/index.d.ts +82 -1
- package/dist/index.js +7 -1
- package/dist/mcp.js +2 -2
- package/package.json +1 -1
|
@@ -1145,6 +1145,103 @@ async function createNostrPoolTransport(urls = DEFAULT_RELAYS) {
|
|
|
1145
1145
|
};
|
|
1146
1146
|
}
|
|
1147
1147
|
|
|
1148
|
+
// src/room.ts
|
|
1149
|
+
import { createHash as createHash3 } from "crypto";
|
|
1150
|
+
var ROOM_TOPIC_PREFIX = "vibedate-room:";
|
|
1151
|
+
function roomTopic(name) {
|
|
1152
|
+
return createHash3("sha256").update(`${ROOM_TOPIC_PREFIX}${name}`, "utf8").digest();
|
|
1153
|
+
}
|
|
1154
|
+
async function startRoom(opts) {
|
|
1155
|
+
const topic = roomTopic(opts.room);
|
|
1156
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1157
|
+
const memberHellos = /* @__PURE__ */ new Map();
|
|
1158
|
+
const messageCbs = /* @__PURE__ */ new Set();
|
|
1159
|
+
const rosterCbs = /* @__PURE__ */ new Set();
|
|
1160
|
+
const signalCbs = /* @__PURE__ */ new Set();
|
|
1161
|
+
const fireRoster = () => {
|
|
1162
|
+
const snapshot = [...memberHellos.values()];
|
|
1163
|
+
for (const cb of rosterCbs) cb(snapshot);
|
|
1164
|
+
};
|
|
1165
|
+
let discovery;
|
|
1166
|
+
const ready = startDiscovery({
|
|
1167
|
+
hello: opts.hello,
|
|
1168
|
+
// ONE topic — the room's own. Rooms are intentionally cross-league, so
|
|
1169
|
+
// every member of the room is accepted regardless of advertised league.
|
|
1170
|
+
topics: [topic],
|
|
1171
|
+
acceptLeague: () => true,
|
|
1172
|
+
...opts.isBlocked === void 0 ? {} : { isBlocked: opts.isBlocked },
|
|
1173
|
+
...opts.bootstrap === void 0 ? {} : { bootstrap: opts.bootstrap },
|
|
1174
|
+
...opts.stateDir === void 0 ? {} : { stateDir: opts.stateDir },
|
|
1175
|
+
...opts.notify === void 0 ? {} : { notify: opts.notify },
|
|
1176
|
+
onLink: (link) => {
|
|
1177
|
+
const handle = link.hello.handle;
|
|
1178
|
+
entries.set(handle, { hello: link.hello, link });
|
|
1179
|
+
memberHellos.set(handle, link.hello);
|
|
1180
|
+
fireRoster();
|
|
1181
|
+
link.onMessage((m) => {
|
|
1182
|
+
for (const cb of messageCbs) cb({ from: handle, ...m });
|
|
1183
|
+
});
|
|
1184
|
+
link.onSignal((frame) => {
|
|
1185
|
+
for (const cb of signalCbs) cb(handle, frame);
|
|
1186
|
+
});
|
|
1187
|
+
link.onClose(() => {
|
|
1188
|
+
const cur = entries.get(handle);
|
|
1189
|
+
if (cur && cur.link === link) {
|
|
1190
|
+
entries.delete(handle);
|
|
1191
|
+
memberHellos.delete(handle);
|
|
1192
|
+
fireRoster();
|
|
1193
|
+
}
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
}).then((s) => {
|
|
1197
|
+
discovery = s;
|
|
1198
|
+
return s.ready;
|
|
1199
|
+
});
|
|
1200
|
+
const session = {
|
|
1201
|
+
room: opts.room,
|
|
1202
|
+
topic,
|
|
1203
|
+
hello: opts.hello,
|
|
1204
|
+
members: memberHellos,
|
|
1205
|
+
ready,
|
|
1206
|
+
broadcast(text) {
|
|
1207
|
+
const reached = [];
|
|
1208
|
+
for (const [handle, entry] of entries) {
|
|
1209
|
+
entry.link.send(text);
|
|
1210
|
+
reached.push(handle);
|
|
1211
|
+
}
|
|
1212
|
+
return reached;
|
|
1213
|
+
},
|
|
1214
|
+
onMessage(cb) {
|
|
1215
|
+
messageCbs.add(cb);
|
|
1216
|
+
},
|
|
1217
|
+
onRoster(cb) {
|
|
1218
|
+
rosterCbs.add(cb);
|
|
1219
|
+
},
|
|
1220
|
+
onSignal(cb) {
|
|
1221
|
+
signalCbs.add(cb);
|
|
1222
|
+
},
|
|
1223
|
+
sendSignal(handle, frame) {
|
|
1224
|
+
entries.get(handle)?.link.sendSignal(frame);
|
|
1225
|
+
},
|
|
1226
|
+
linkFor(handle) {
|
|
1227
|
+
return entries.get(handle)?.link;
|
|
1228
|
+
},
|
|
1229
|
+
async close() {
|
|
1230
|
+
await ready.catch(() => void 0);
|
|
1231
|
+
for (const entry of entries.values()) {
|
|
1232
|
+
try {
|
|
1233
|
+
entry.link.close();
|
|
1234
|
+
} catch {
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
entries.clear();
|
|
1238
|
+
memberHellos.clear();
|
|
1239
|
+
if (discovery !== void 0) await discovery.close();
|
|
1240
|
+
}
|
|
1241
|
+
};
|
|
1242
|
+
return session;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1148
1245
|
// src/index.ts
|
|
1149
1246
|
var LEAGUES = [
|
|
1150
1247
|
{ name: "1M", min: 1e6, max: 4999999 },
|
|
@@ -1320,6 +1417,9 @@ export {
|
|
|
1320
1417
|
conversationTag,
|
|
1321
1418
|
createNostrRelayLink,
|
|
1322
1419
|
createNostrPoolTransport,
|
|
1420
|
+
ROOM_TOPIC_PREFIX,
|
|
1421
|
+
roomTopic,
|
|
1422
|
+
startRoom,
|
|
1323
1423
|
LEAGUES,
|
|
1324
1424
|
BELOW_LEAGUE,
|
|
1325
1425
|
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' | 'daemon' | 'mcp' | 'help' | 'version' | null;
|
|
3
|
+
type Command = 'connect' | 'matches' | 'discover' | 'open' | 'live' | 'find' | 'handle' | 'block' | 'unblock' | 'blocklist' | 'daemon' | 'mcp' | 'room' | 'help' | 'version' | null;
|
|
4
4
|
interface ParsedArgs {
|
|
5
5
|
readonly command: Command;
|
|
6
6
|
/** Port for `open --port`; undefined means "let the OS pick". */
|
|
@@ -23,6 +23,8 @@ interface ParsedArgs {
|
|
|
23
23
|
* auto-fallback-on-timeout is a follow-up. Default false.
|
|
24
24
|
*/
|
|
25
25
|
readonly viaRelay: boolean;
|
|
26
|
+
/** `room <name>` positional, or `--room <name>`: join/create a named room. */
|
|
27
|
+
readonly room: string | undefined;
|
|
26
28
|
}
|
|
27
29
|
/**
|
|
28
30
|
* Parse argv (the slice AFTER the program name) into a command + options.
|
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
runMcp
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-ZD6JBWEW.js";
|
|
5
5
|
import {
|
|
6
6
|
CANDIDATES,
|
|
7
7
|
DEFAULT_HANDLE,
|
|
8
8
|
LIVE_NOTICE,
|
|
9
9
|
MAX_HANDLE_LEN,
|
|
10
|
+
ROOM_TOPIC_PREFIX,
|
|
10
11
|
TOPIC_PREFIX,
|
|
11
12
|
addBlock,
|
|
12
13
|
allLeagueNames,
|
|
@@ -37,8 +38,9 @@ import {
|
|
|
37
38
|
sanitizePeerText,
|
|
38
39
|
saveHandle,
|
|
39
40
|
signHelloClaims,
|
|
40
|
-
startDiscovery
|
|
41
|
-
|
|
41
|
+
startDiscovery,
|
|
42
|
+
startRoom
|
|
43
|
+
} from "./chunk-HDHJLZZG.js";
|
|
42
44
|
|
|
43
45
|
// src/cli.ts
|
|
44
46
|
import readline from "readline";
|
|
@@ -1891,6 +1893,111 @@ function createLiveBridge() {
|
|
|
1891
1893
|
};
|
|
1892
1894
|
return bridge;
|
|
1893
1895
|
}
|
|
1896
|
+
var MAX_QUEUED_ROOM_SIGNALS = 200;
|
|
1897
|
+
function createRoomBridge(name) {
|
|
1898
|
+
let session;
|
|
1899
|
+
const messageQueue = [];
|
|
1900
|
+
const messageWaiters = [];
|
|
1901
|
+
const signalBoxes = /* @__PURE__ */ new Map();
|
|
1902
|
+
const signalWaiters = /* @__PURE__ */ new Map();
|
|
1903
|
+
const drainMessage = (m) => {
|
|
1904
|
+
const safe = { ...m, text: sanitizePeerText(m.text) };
|
|
1905
|
+
const waiter = messageWaiters.shift();
|
|
1906
|
+
if (waiter !== void 0) {
|
|
1907
|
+
waiter(safe);
|
|
1908
|
+
return;
|
|
1909
|
+
}
|
|
1910
|
+
messageQueue.push(safe);
|
|
1911
|
+
if (messageQueue.length > MAX_QUEUED_MESSAGES) {
|
|
1912
|
+
messageQueue.splice(0, messageQueue.length - MAX_QUEUED_MESSAGES);
|
|
1913
|
+
}
|
|
1914
|
+
};
|
|
1915
|
+
const drainSignal = (from, frame) => {
|
|
1916
|
+
const waiters = signalWaiters.get(from);
|
|
1917
|
+
if (waiters !== void 0 && waiters.length > 0) {
|
|
1918
|
+
const w = waiters.shift();
|
|
1919
|
+
if (w !== void 0) {
|
|
1920
|
+
if (waiters.length === 0) signalWaiters.delete(from);
|
|
1921
|
+
w(frame);
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
let box = signalBoxes.get(from);
|
|
1926
|
+
if (box === void 0) {
|
|
1927
|
+
box = [];
|
|
1928
|
+
signalBoxes.set(from, box);
|
|
1929
|
+
}
|
|
1930
|
+
box.push(frame);
|
|
1931
|
+
if (box.length > MAX_QUEUED_ROOM_SIGNALS) {
|
|
1932
|
+
box.splice(0, box.length - MAX_QUEUED_ROOM_SIGNALS);
|
|
1933
|
+
}
|
|
1934
|
+
};
|
|
1935
|
+
return {
|
|
1936
|
+
name,
|
|
1937
|
+
get self() {
|
|
1938
|
+
return session?.hello.handle;
|
|
1939
|
+
},
|
|
1940
|
+
get members() {
|
|
1941
|
+
if (session === void 0) return [];
|
|
1942
|
+
return [...session.members.values()].map((h) => ({
|
|
1943
|
+
handle: h.handle,
|
|
1944
|
+
league: h.league,
|
|
1945
|
+
harness: h.harness,
|
|
1946
|
+
...h.verified !== void 0 ? { verified: h.verified } : {},
|
|
1947
|
+
...h.identityVerified !== void 0 ? { identityVerified: h.identityVerified } : {}
|
|
1948
|
+
}));
|
|
1949
|
+
},
|
|
1950
|
+
attach(s) {
|
|
1951
|
+
session = s;
|
|
1952
|
+
s.onMessage(drainMessage);
|
|
1953
|
+
s.onSignal((from, frame) => drainSignal(from, frame));
|
|
1954
|
+
},
|
|
1955
|
+
broadcast(text) {
|
|
1956
|
+
session?.broadcast(text);
|
|
1957
|
+
},
|
|
1958
|
+
async pollMessage(timeoutMs) {
|
|
1959
|
+
if (messageQueue.length > 0) return messageQueue.shift() ?? null;
|
|
1960
|
+
return new Promise((resolve) => {
|
|
1961
|
+
const timer = setTimeout(() => {
|
|
1962
|
+
const idx = messageWaiters.indexOf(resolve);
|
|
1963
|
+
if (idx >= 0) messageWaiters.splice(idx, 1);
|
|
1964
|
+
resolve(null);
|
|
1965
|
+
}, timeoutMs);
|
|
1966
|
+
messageWaiters.push((m) => {
|
|
1967
|
+
clearTimeout(timer);
|
|
1968
|
+
resolve(m);
|
|
1969
|
+
});
|
|
1970
|
+
});
|
|
1971
|
+
},
|
|
1972
|
+
sendSignal(handle2, frame) {
|
|
1973
|
+
session?.sendSignal(handle2, frame);
|
|
1974
|
+
},
|
|
1975
|
+
async pollSignal(handle2, timeoutMs) {
|
|
1976
|
+
const box = signalBoxes.get(handle2);
|
|
1977
|
+
if (box !== void 0 && box.length > 0) return box.shift() ?? null;
|
|
1978
|
+
return new Promise((resolve) => {
|
|
1979
|
+
const timer = setTimeout(() => {
|
|
1980
|
+
const waiters2 = signalWaiters.get(handle2);
|
|
1981
|
+
if (waiters2 !== void 0) {
|
|
1982
|
+
const idx = waiters2.indexOf(resolve);
|
|
1983
|
+
if (idx >= 0) waiters2.splice(idx, 1);
|
|
1984
|
+
if (waiters2.length === 0) signalWaiters.delete(handle2);
|
|
1985
|
+
}
|
|
1986
|
+
resolve(null);
|
|
1987
|
+
}, timeoutMs);
|
|
1988
|
+
let waiters = signalWaiters.get(handle2);
|
|
1989
|
+
if (waiters === void 0) {
|
|
1990
|
+
waiters = [];
|
|
1991
|
+
signalWaiters.set(handle2, waiters);
|
|
1992
|
+
}
|
|
1993
|
+
waiters.push((f) => {
|
|
1994
|
+
clearTimeout(timer);
|
|
1995
|
+
resolve(f);
|
|
1996
|
+
});
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
1894
2001
|
function currentState(dir) {
|
|
1895
2002
|
const p = loadProfile(dir);
|
|
1896
2003
|
if (!p) return { connected: false, candidates: [] };
|
|
@@ -2111,11 +2218,101 @@ async function handle(req, res, opts) {
|
|
|
2111
2218
|
sendJson(res, 200, { ok: true });
|
|
2112
2219
|
return;
|
|
2113
2220
|
}
|
|
2221
|
+
if (req.method === "GET" && pathname === "/api/room") {
|
|
2222
|
+
const room = opts.room;
|
|
2223
|
+
sendJson(res, 200, room ? { room: room.name, self: room.self ?? null, members: room.members } : { room: null, self: null, members: [] });
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
if (req.method === "GET" && pathname === "/room/message") {
|
|
2227
|
+
const room = opts.room;
|
|
2228
|
+
if (!room) {
|
|
2229
|
+
sendJson(res, 200, { message: null, reason: "room-not-attached" });
|
|
2230
|
+
return;
|
|
2231
|
+
}
|
|
2232
|
+
const message = await room.pollMessage(25e3);
|
|
2233
|
+
if (req.destroyed || res.writableEnded) return;
|
|
2234
|
+
sendJson(res, 200, { message });
|
|
2235
|
+
return;
|
|
2236
|
+
}
|
|
2237
|
+
if (req.method === "POST" && pathname === "/room/message") {
|
|
2238
|
+
const room = opts.room;
|
|
2239
|
+
if (!room) {
|
|
2240
|
+
sendJson(res, 400, { error: "room-not-attached" });
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
2243
|
+
const body = await readBody(req);
|
|
2244
|
+
let parsed = {};
|
|
2245
|
+
try {
|
|
2246
|
+
parsed = JSON.parse(body);
|
|
2247
|
+
} catch {
|
|
2248
|
+
sendJson(res, 400, { error: "invalid JSON body" });
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
2251
|
+
const text = parsed["text"];
|
|
2252
|
+
if (typeof text !== "string") {
|
|
2253
|
+
sendJson(res, 400, { error: "missing text" });
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
const reParsed = parseFrame(
|
|
2257
|
+
JSON.stringify({ t: "msg", id: randomUUID(), text, at: Date.now() })
|
|
2258
|
+
);
|
|
2259
|
+
if (reParsed === null || reParsed.t !== "msg") {
|
|
2260
|
+
sendJson(res, 400, { error: "invalid message text" });
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
room.broadcast(reParsed.text);
|
|
2264
|
+
sendJson(res, 200, { ok: true });
|
|
2265
|
+
return;
|
|
2266
|
+
}
|
|
2267
|
+
if (req.method === "GET" && pathname === "/room/signal") {
|
|
2268
|
+
const room = opts.room;
|
|
2269
|
+
if (!room) {
|
|
2270
|
+
sendJson(res, 200, { frame: null, reason: "room-not-attached" });
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2273
|
+
const handle2 = url.searchParams.get("handle") ?? "";
|
|
2274
|
+
if (handle2 === "") {
|
|
2275
|
+
sendJson(res, 400, { error: "missing handle" });
|
|
2276
|
+
return;
|
|
2277
|
+
}
|
|
2278
|
+
const frame = await room.pollSignal(handle2, 25e3);
|
|
2279
|
+
if (req.destroyed || res.writableEnded) return;
|
|
2280
|
+
sendJson(res, 200, { frame });
|
|
2281
|
+
return;
|
|
2282
|
+
}
|
|
2283
|
+
if (req.method === "POST" && pathname === "/room/signal") {
|
|
2284
|
+
const room = opts.room;
|
|
2285
|
+
if (!room) {
|
|
2286
|
+
sendJson(res, 400, { error: "room-not-attached" });
|
|
2287
|
+
return;
|
|
2288
|
+
}
|
|
2289
|
+
const body = await readBody(req);
|
|
2290
|
+
let parsed = {};
|
|
2291
|
+
try {
|
|
2292
|
+
parsed = JSON.parse(body);
|
|
2293
|
+
} catch {
|
|
2294
|
+
sendJson(res, 400, { error: "invalid JSON body" });
|
|
2295
|
+
return;
|
|
2296
|
+
}
|
|
2297
|
+
const handle2 = typeof parsed["handle"] === "string" ? parsed["handle"] : "";
|
|
2298
|
+
if (handle2 === "") {
|
|
2299
|
+
sendJson(res, 400, { error: "missing handle" });
|
|
2300
|
+
return;
|
|
2301
|
+
}
|
|
2302
|
+
const reParsed = parseFrame(JSON.stringify(parsed["frame"]));
|
|
2303
|
+
if (reParsed === null || reParsed.t !== "rtc-offer" && reParsed.t !== "rtc-answer" && reParsed.t !== "rtc-ice") {
|
|
2304
|
+
sendJson(res, 400, { error: "invalid rtc frame" });
|
|
2305
|
+
return;
|
|
2306
|
+
}
|
|
2307
|
+
room.sendSignal(handle2, reParsed);
|
|
2308
|
+
sendJson(res, 200, { ok: true });
|
|
2309
|
+
return;
|
|
2310
|
+
}
|
|
2114
2311
|
sendJson(res, 404, { error: "not found" });
|
|
2115
2312
|
}
|
|
2116
2313
|
|
|
2117
2314
|
// src/cli.ts
|
|
2118
|
-
var VERSION = "0.
|
|
2315
|
+
var VERSION = "0.7.0";
|
|
2119
2316
|
function parsePort(raw) {
|
|
2120
2317
|
const n = Number(raw);
|
|
2121
2318
|
if (!Number.isInteger(n) || n < 1 || n > 65535) return void 0;
|
|
@@ -2131,7 +2328,8 @@ function parseArgs(argv) {
|
|
|
2131
2328
|
arg: void 0,
|
|
2132
2329
|
to: void 0,
|
|
2133
2330
|
keepAlive: false,
|
|
2134
|
-
viaRelay: false
|
|
2331
|
+
viaRelay: false,
|
|
2332
|
+
room: void 0
|
|
2135
2333
|
};
|
|
2136
2334
|
for (let i = 0; i < argv.length; i++) {
|
|
2137
2335
|
const a = argv[i];
|
|
@@ -2145,7 +2343,8 @@ function parseArgs(argv) {
|
|
|
2145
2343
|
arg: void 0,
|
|
2146
2344
|
to: void 0,
|
|
2147
2345
|
keepAlive: false,
|
|
2148
|
-
viaRelay: false
|
|
2346
|
+
viaRelay: false,
|
|
2347
|
+
room: void 0
|
|
2149
2348
|
};
|
|
2150
2349
|
}
|
|
2151
2350
|
if (a === "--help" || a === "-h") {
|
|
@@ -2158,7 +2357,8 @@ function parseArgs(argv) {
|
|
|
2158
2357
|
arg: void 0,
|
|
2159
2358
|
to: void 0,
|
|
2160
2359
|
keepAlive: false,
|
|
2161
|
-
viaRelay: false
|
|
2360
|
+
viaRelay: false,
|
|
2361
|
+
room: void 0
|
|
2162
2362
|
};
|
|
2163
2363
|
}
|
|
2164
2364
|
if (a === "--live") {
|
|
@@ -2173,6 +2373,18 @@ function parseArgs(argv) {
|
|
|
2173
2373
|
out = { ...out, viaRelay: true };
|
|
2174
2374
|
continue;
|
|
2175
2375
|
}
|
|
2376
|
+
if (a === "--room") {
|
|
2377
|
+
const next = argv[i + 1];
|
|
2378
|
+
if (next !== void 0) {
|
|
2379
|
+
out = { ...out, room: next };
|
|
2380
|
+
i++;
|
|
2381
|
+
}
|
|
2382
|
+
continue;
|
|
2383
|
+
}
|
|
2384
|
+
if (a.startsWith("--room=")) {
|
|
2385
|
+
out = { ...out, room: a.slice("--room=".length) };
|
|
2386
|
+
continue;
|
|
2387
|
+
}
|
|
2176
2388
|
if (a === "--any") {
|
|
2177
2389
|
out = { ...out, any: true };
|
|
2178
2390
|
continue;
|
|
@@ -2208,7 +2420,7 @@ function parseArgs(argv) {
|
|
|
2208
2420
|
continue;
|
|
2209
2421
|
}
|
|
2210
2422
|
if (a.startsWith("-")) continue;
|
|
2211
|
-
const known = a === "connect" || a === "matches" || a === "discover" || a === "open" || a === "live" || a === "find" || a === "handle" || a === "block" || a === "unblock" || a === "blocklist" || a === "daemon" || a === "mcp" || a === "help" ? a : null;
|
|
2423
|
+
const known = a === "connect" || a === "matches" || a === "discover" || a === "open" || a === "live" || a === "find" || a === "handle" || a === "block" || a === "unblock" || a === "blocklist" || a === "daemon" || a === "mcp" || a === "room" || a === "help" ? a : null;
|
|
2212
2424
|
if (known !== null && out.command === null) {
|
|
2213
2425
|
out = { ...out, command: known };
|
|
2214
2426
|
} else if (out.arg === void 0) {
|
|
@@ -2452,36 +2664,59 @@ async function cmdDiscover(live, any, viaRelay) {
|
|
|
2452
2664
|
);
|
|
2453
2665
|
return 0;
|
|
2454
2666
|
}
|
|
2455
|
-
async function cmdOpen(port, any) {
|
|
2667
|
+
async function cmdOpen(port, any, room) {
|
|
2456
2668
|
const profile = loadProfile();
|
|
2457
2669
|
let live;
|
|
2670
|
+
let roomBridge;
|
|
2458
2671
|
let session;
|
|
2672
|
+
let roomSession;
|
|
2459
2673
|
if (profile) {
|
|
2460
2674
|
if (!canShareLive()) grantLiveConsent();
|
|
2461
|
-
|
|
2675
|
+
if (room !== void 0) {
|
|
2676
|
+
roomBridge = createRoomBridge(room);
|
|
2677
|
+
} else {
|
|
2678
|
+
live = createLiveBridge();
|
|
2679
|
+
}
|
|
2462
2680
|
}
|
|
2463
|
-
const started = await startServer({
|
|
2464
|
-
|
|
2681
|
+
const started = await startServer({
|
|
2682
|
+
port,
|
|
2683
|
+
live,
|
|
2684
|
+
...roomBridge === void 0 ? {} : { room: roomBridge }
|
|
2685
|
+
});
|
|
2686
|
+
if (profile) {
|
|
2465
2687
|
process2.stdout.write(`
|
|
2466
2688
|
${LIVE_NOTICE}
|
|
2467
2689
|
`);
|
|
2468
2690
|
const hello = buildHello(profile);
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2691
|
+
if (room !== void 0 && roomBridge !== void 0) {
|
|
2692
|
+
void startRoom({ hello, room, isBlocked: blockedChecker() }).then((s) => {
|
|
2693
|
+
roomSession = s;
|
|
2694
|
+
roomBridge.attach(s);
|
|
2695
|
+
}).catch(() => {
|
|
2696
|
+
});
|
|
2697
|
+
} else if (live) {
|
|
2698
|
+
const { topics, acceptLeague } = discoveryScope(profile.league, any);
|
|
2699
|
+
void startDiscovery({
|
|
2700
|
+
hello,
|
|
2701
|
+
topics,
|
|
2702
|
+
acceptLeague,
|
|
2703
|
+
isBlocked: blockedChecker(),
|
|
2704
|
+
onLink: (link) => live.addLink(link)
|
|
2705
|
+
}).then((s) => {
|
|
2706
|
+
session = s;
|
|
2707
|
+
}).catch(() => {
|
|
2708
|
+
});
|
|
2709
|
+
}
|
|
2480
2710
|
}
|
|
2481
2711
|
process2.stdout.write(`
|
|
2482
2712
|
vibedating local web app \u2192 ${started.url}
|
|
2483
2713
|
`);
|
|
2484
|
-
if (
|
|
2714
|
+
if (room !== void 0) {
|
|
2715
|
+
process2.stdout.write(
|
|
2716
|
+
` \u2022 room: ${room} \u2014 roster + group chat + full-mesh group video (~6 people; an SFU is the upgrade path for bigger rooms)
|
|
2717
|
+
`
|
|
2718
|
+
);
|
|
2719
|
+
} else if (live) {
|
|
2485
2720
|
process2.stdout.write(
|
|
2486
2721
|
any ? " \u2022 live video + chat available for connected peers (ANY league \u2014 --any)\n" : " \u2022 live video + chat available for connected peers (your league + adjacent; --any = everyone)\n"
|
|
2487
2722
|
);
|
|
@@ -2495,6 +2730,7 @@ async function cmdOpen(port, any) {
|
|
|
2495
2730
|
process2.once("SIGTERM", () => resolve());
|
|
2496
2731
|
});
|
|
2497
2732
|
process2.stdout.write("\n shutting down\u2026\n");
|
|
2733
|
+
if (roomSession) await roomSession.close();
|
|
2498
2734
|
if (session) await session.close();
|
|
2499
2735
|
await new Promise((resolve) => started.server.close(() => resolve()));
|
|
2500
2736
|
return 0;
|
|
@@ -2717,6 +2953,104 @@ async function cmdLive(dating, any, to, keepAlive, viaRelay) {
|
|
|
2717
2953
|
process2.stdout.write("\n");
|
|
2718
2954
|
return 0;
|
|
2719
2955
|
}
|
|
2956
|
+
async function cmdRoom(name, keepAlive) {
|
|
2957
|
+
if (name === void 0 || name.trim() === "") {
|
|
2958
|
+
process2.stderr.write("usage: vibedating room <name>\n");
|
|
2959
|
+
return 1;
|
|
2960
|
+
}
|
|
2961
|
+
const profile = loadProfile();
|
|
2962
|
+
if (!profile) {
|
|
2963
|
+
process2.stderr.write("Not connected yet. Run `vibedating connect` first.\n");
|
|
2964
|
+
return 1;
|
|
2965
|
+
}
|
|
2966
|
+
if (!canShareLive()) grantLiveConsent();
|
|
2967
|
+
if (!canShareLive()) {
|
|
2968
|
+
process2.stderr.write("Could not enable live discovery. Try `vibedating discover --live`.\n");
|
|
2969
|
+
return 1;
|
|
2970
|
+
}
|
|
2971
|
+
const hello = buildHello(profile);
|
|
2972
|
+
process2.stdout.write("\n");
|
|
2973
|
+
process2.stdout.write(` ${LIVE_NOTICE}
|
|
2974
|
+
`);
|
|
2975
|
+
process2.stdout.write(` ${MARKS_LEGEND}
|
|
2976
|
+
`);
|
|
2977
|
+
process2.stdout.write(` room: ${name} (cross-league \u2014 everyone in the room is a member)
|
|
2978
|
+
`);
|
|
2979
|
+
const session = await startRoom({
|
|
2980
|
+
hello,
|
|
2981
|
+
room: name,
|
|
2982
|
+
isBlocked: blockedChecker()
|
|
2983
|
+
});
|
|
2984
|
+
session.onRoster((members) => {
|
|
2985
|
+
if (members.length === 0) {
|
|
2986
|
+
process2.stdout.write(" \xB7 room empty \u2014 waiting for members\u2026\n");
|
|
2987
|
+
return;
|
|
2988
|
+
}
|
|
2989
|
+
const list = members.map(
|
|
2990
|
+
(m) => `${sanitizePeerText(m.handle)} (${m.league} \xB7 ${m.harness}) ${usageMark(m)}${idMark(m)}`
|
|
2991
|
+
).join(", ");
|
|
2992
|
+
process2.stdout.write(` \xB7 room (${members.length}): ${list}
|
|
2993
|
+
`);
|
|
2994
|
+
});
|
|
2995
|
+
session.onMessage((m) => {
|
|
2996
|
+
process2.stdout.write(` <${sanitizePeerText(m.from)}> ${sanitizePeerText(m.text)}
|
|
2997
|
+
`);
|
|
2998
|
+
});
|
|
2999
|
+
process2.stdout.write(
|
|
3000
|
+
` topic: ${ROOM_TOPIC_PREFIX}${name} \u2192 ${session.topic.toString("hex").slice(0, 12)}\u2026
|
|
3001
|
+
`
|
|
3002
|
+
);
|
|
3003
|
+
process2.stdout.write(" type to broadcast to the whole room \xB7 /who \xB7 /quit\n");
|
|
3004
|
+
process2.stdout.write(` group video: run \`vibedating open --room ${name}\`
|
|
3005
|
+
|
|
3006
|
+
`);
|
|
3007
|
+
const rl = readline.createInterface({ input: process2.stdin, terminal: false });
|
|
3008
|
+
const stop = () => {
|
|
3009
|
+
rl.close();
|
|
3010
|
+
};
|
|
3011
|
+
process2.once("SIGINT", stop);
|
|
3012
|
+
process2.once("SIGTERM", stop);
|
|
3013
|
+
let quitRequested = false;
|
|
3014
|
+
for await (const line of rl) {
|
|
3015
|
+
const text = line.trim();
|
|
3016
|
+
if (text === "/quit") {
|
|
3017
|
+
quitRequested = true;
|
|
3018
|
+
break;
|
|
3019
|
+
}
|
|
3020
|
+
if (text === "/who") {
|
|
3021
|
+
const members = [...session.members.values()];
|
|
3022
|
+
if (members.length === 0) {
|
|
3023
|
+
process2.stdout.write(" \xB7 room empty\n");
|
|
3024
|
+
} else {
|
|
3025
|
+
for (const m of members) {
|
|
3026
|
+
process2.stdout.write(
|
|
3027
|
+
` \xB7 ${sanitizePeerText(m.handle)} (${m.league} \xB7 ${m.harness}) ${usageMark(m)}${idMark(m)}
|
|
3028
|
+
`
|
|
3029
|
+
);
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
continue;
|
|
3033
|
+
}
|
|
3034
|
+
if (text === "") continue;
|
|
3035
|
+
const reached = session.broadcast(text);
|
|
3036
|
+
if (reached.length === 0) {
|
|
3037
|
+
process2.stdout.write(" \xB7 room empty \u2014 no one to hear you yet\n");
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
if (!quitRequested && shouldKeepAlive(keepAlive, process2.stdin.isTTY)) {
|
|
3041
|
+
process2.stdout.write(" stdin closed \u2014 staying in the room (Ctrl+C / SIGTERM to leave)\n");
|
|
3042
|
+
await new Promise((resolve) => {
|
|
3043
|
+
process2.once("SIGINT", () => resolve());
|
|
3044
|
+
process2.once("SIGTERM", () => resolve());
|
|
3045
|
+
});
|
|
3046
|
+
}
|
|
3047
|
+
process2.removeListener("SIGINT", stop);
|
|
3048
|
+
process2.removeListener("SIGTERM", stop);
|
|
3049
|
+
process2.stdout.write("\n leaving the room\u2026\n");
|
|
3050
|
+
await session.close();
|
|
3051
|
+
process2.stdout.write("\n");
|
|
3052
|
+
return 0;
|
|
3053
|
+
}
|
|
2720
3054
|
async function cmdFind(targetArg, any) {
|
|
2721
3055
|
if (targetArg === void 0 || targetArg.trim() === "") {
|
|
2722
3056
|
process2.stderr.write("usage: vibedating find <@handle> [--any]\n");
|
|
@@ -2968,9 +3302,15 @@ Usage:
|
|
|
2968
3302
|
NEW matches, never opens chat/video. install adds a
|
|
2969
3303
|
login service (launchd on macOS, systemd on Linux);
|
|
2970
3304
|
uninstall removes it.
|
|
2971
|
-
vibedating
|
|
2972
|
-
+
|
|
2973
|
-
|
|
3305
|
+
vibedating room <name> Join/create a named room (multi-peer): live roster
|
|
3306
|
+
+ group text chat broadcast to all members.
|
|
3307
|
+
Consent-gated like live. /who lists members, /quit leaves.
|
|
3308
|
+
vibedating open [--port N] [--any] [--room <name>] Serve the local web app (default:
|
|
3309
|
+
random port) + live video + chat with connected peers
|
|
3310
|
+
(your league + adjacent; --any = every league).
|
|
3311
|
+
--room <name> opens the room view instead: roster +
|
|
3312
|
+
group chat + full-mesh group video (~6 people; an SFU
|
|
3313
|
+
is the upgrade path for bigger rooms).
|
|
2974
3314
|
vibedating mcp Run the stdio MCP server (profile, matches)
|
|
2975
3315
|
vibedating --version
|
|
2976
3316
|
vibedating --help
|
|
@@ -3022,11 +3362,13 @@ async function main(argv) {
|
|
|
3022
3362
|
case "discover":
|
|
3023
3363
|
return cmdDiscover(parsed.live, parsed.any, parsed.viaRelay);
|
|
3024
3364
|
case "open":
|
|
3025
|
-
return cmdOpen(parsed.port, parsed.any);
|
|
3365
|
+
return cmdOpen(parsed.port, parsed.any, parsed.room);
|
|
3026
3366
|
case "live":
|
|
3027
3367
|
return cmdLive(parsed.dating, parsed.any, parsed.to, parsed.keepAlive, parsed.viaRelay);
|
|
3028
3368
|
case "find":
|
|
3029
3369
|
return cmdFind(parsed.arg, parsed.any);
|
|
3370
|
+
case "room":
|
|
3371
|
+
return cmdRoom(parsed.arg ?? parsed.room, parsed.keepAlive);
|
|
3030
3372
|
case "mcp":
|
|
3031
3373
|
await runMcp();
|
|
3032
3374
|
return 0;
|
package/dist/index.d.ts
CHANGED
|
@@ -351,6 +351,87 @@ declare function createNostrRelayLink(opts: CreateNostrRelayLinkOptions): Promis
|
|
|
351
351
|
*/
|
|
352
352
|
declare function createNostrPoolTransport(urls?: readonly string[]): Promise<RelayTransport>;
|
|
353
353
|
|
|
354
|
+
/** Namespace prefix so room topics never collide with league topics (or anything
|
|
355
|
+
* else on the DHT). Mirrors {@link p2p.TOPIC_PREFIX} for the 1:1 path. */
|
|
356
|
+
declare const ROOM_TOPIC_PREFIX = "vibedate-room:";
|
|
357
|
+
/**
|
|
358
|
+
* Derive the 32-byte DHT topic for a named room. Deterministic: everyone who
|
|
359
|
+
* joins the same room name anywhere in the world hashes to the same topic,
|
|
360
|
+
* which is the entire discovery mechanism. Pure (mirrors {@link p2p.leagueTopic}).
|
|
361
|
+
*/
|
|
362
|
+
declare function roomTopic(name: string): Buffer;
|
|
363
|
+
/** A room member = a connected, handshaken peer. Same shape as a live peer. */
|
|
364
|
+
type RoomMember = PeerHello;
|
|
365
|
+
/** One group chat message: a `msg` frame's payload tagged with the sender. */
|
|
366
|
+
interface RoomMessage {
|
|
367
|
+
/** Sender's handle (from the validated hello — UNTRUSTED display data). */
|
|
368
|
+
readonly from: string;
|
|
369
|
+
readonly id: string;
|
|
370
|
+
readonly text: string;
|
|
371
|
+
readonly at: number;
|
|
372
|
+
}
|
|
373
|
+
interface RoomOptions {
|
|
374
|
+
/** What we broadcast. Must already be consent-gated by the caller. */
|
|
375
|
+
readonly hello: PeerHello;
|
|
376
|
+
/** Room name → its own DHT topic (see {@link roomTopic}). */
|
|
377
|
+
readonly room: string;
|
|
378
|
+
/** DHT bootstrap nodes; omit for the public DHT. Tests pass a local testnet. */
|
|
379
|
+
readonly bootstrap?: ReadonlyArray<{
|
|
380
|
+
readonly host: string;
|
|
381
|
+
readonly port: number;
|
|
382
|
+
}>;
|
|
383
|
+
/** Where peers.json lives. Defaults to ~/.vibedating. */
|
|
384
|
+
readonly stateDir?: string;
|
|
385
|
+
/** Predicate over a blocked handle — a blocked peer's hello is dropped exactly
|
|
386
|
+
* like in 1:1 discovery. Default: nothing blocked. */
|
|
387
|
+
readonly isBlocked?: (handle: string) => boolean;
|
|
388
|
+
/** Match-notification sink (tests capture with a fake). Best-effort. */
|
|
389
|
+
readonly notify?: NotifySink;
|
|
390
|
+
}
|
|
391
|
+
interface RoomSession {
|
|
392
|
+
/** The room name. */
|
|
393
|
+
readonly room: string;
|
|
394
|
+
/** The joined DHT topic (see {@link roomTopic}). */
|
|
395
|
+
readonly topic: Buffer;
|
|
396
|
+
/** What we broadcast. */
|
|
397
|
+
readonly hello: PeerHello;
|
|
398
|
+
/** Live member set, keyed by handle (excludes self). Mirrors the live view
|
|
399
|
+
* semantics of {@link p2p.DiscoverySession.peers}. */
|
|
400
|
+
readonly members: ReadonlyMap<string, RoomMember>;
|
|
401
|
+
/** Resolves when the first DHT announce/lookup round completes. */
|
|
402
|
+
readonly ready: Promise<unknown>;
|
|
403
|
+
/**
|
|
404
|
+
* Broadcast a text message to ALL room members (fan-out: one `msg` frame per
|
|
405
|
+
* member's {@link PeerLink}). Returns the handles the message was sent to.
|
|
406
|
+
* Best-effort: a member whose link has just closed is skipped silently.
|
|
407
|
+
*/
|
|
408
|
+
broadcast(text: string): readonly string[];
|
|
409
|
+
/** Register a callback fired for each incoming group message (with sender). */
|
|
410
|
+
onMessage(cb: (m: RoomMessage) => void): void;
|
|
411
|
+
/** Register a callback fired whenever the member roster changes (join/leave). */
|
|
412
|
+
onRoster(cb: (members: readonly RoomMember[]) => void): void;
|
|
413
|
+
/** Register a callback fired for each incoming `rtc-*` signaling frame,
|
|
414
|
+
* tagged with the sender's handle (full-mesh video signaling). */
|
|
415
|
+
onSignal(cb: (from: string, frame: RtcFrame) => void): void;
|
|
416
|
+
/** Relay one `rtc-*` signaling frame to one member (by handle). */
|
|
417
|
+
sendSignal(handle: string, frame: RtcFrame): void;
|
|
418
|
+
/** The underlying {@link PeerLink} to a member (by handle), or undefined. */
|
|
419
|
+
linkFor(handle: string): PeerLink | undefined;
|
|
420
|
+
/** Leave the room and destroy the node. Idempotent. */
|
|
421
|
+
close(): Promise<void>;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Join (or create) a named room on the DHT and discover ALL members. CONSENT
|
|
425
|
+
* GATE LIVES WITH THE CALLER — never call this without the `share:live` grant
|
|
426
|
+
* (or an explicit opt-in in the same breath), exactly like
|
|
427
|
+
* {@link p2p.startDiscovery}.
|
|
428
|
+
*
|
|
429
|
+
* Resolves once the first DHT announce/lookup round completes; the returned
|
|
430
|
+
* session's {@link RoomSession.members} map + {@link RoomSession.onRoster}
|
|
431
|
+
* callback track every member that joins or leaves thereafter.
|
|
432
|
+
*/
|
|
433
|
+
declare function startRoom(opts: RoomOptions): Promise<RoomSession>;
|
|
434
|
+
|
|
354
435
|
/**
|
|
355
436
|
* Persistent ed25519 identity — binds a handle to a keypair so a peer cannot
|
|
356
437
|
* impersonate it.
|
|
@@ -558,4 +639,4 @@ declare const CANDIDATES: readonly Candidate[];
|
|
|
558
639
|
*/
|
|
559
640
|
declare function matches(myLeague: string, candidates?: readonly Candidate[]): Candidate[];
|
|
560
641
|
|
|
561
|
-
export { BELOW_LEAGUE, CANDIDATES, type Candidate, type CreateNostrRelayLinkOptions, DEFAULT_RELAYS, DEMO_TOTAL_TOKENS, type DiscoveryOptions, type DiscoverySession, type HelloClaims, type Identity, type IdentityProof, type IdentityVerdict, LEAGUES, LIVE_NOTICE, type League, type LocalUsageSnapshot, type NostrKey, type NostrRelayLink, type PeerHello, type RelayEvent, type RelayFilter, type RelayTransport, type StoredPeer, TOKENS_ENV, TOPIC_PREFIX, VIBEDATE_MESSAGE_KIND, VIBEDATE_PRESENCE_KIND, allLeagueNames, canonicalHelloClaims, classifyHelloIdentity, conversationTag, createNostrPoolTransport, createNostrRelayLink, league, leagueIndex, leagueTopic, leaguesWithin, loadOrCreateIdentity, loadOrCreateNostrKey, loadPeers, matches, parseHandshake, parseTokensEnv, readUsage, recordPeer, recordPeerMessage, serializeHandshake, signHelloClaims, startDiscovery, verifyHelloClaims };
|
|
642
|
+
export { BELOW_LEAGUE, CANDIDATES, type Candidate, type CreateNostrRelayLinkOptions, DEFAULT_RELAYS, DEMO_TOTAL_TOKENS, type DiscoveryOptions, type DiscoverySession, type HelloClaims, type Identity, type IdentityProof, type IdentityVerdict, LEAGUES, LIVE_NOTICE, type League, type LocalUsageSnapshot, type NostrKey, type NostrRelayLink, type PeerHello, ROOM_TOPIC_PREFIX, type RelayEvent, type RelayFilter, type RelayTransport, type RoomMember, type RoomMessage, type RoomOptions, type RoomSession, type StoredPeer, TOKENS_ENV, TOPIC_PREFIX, VIBEDATE_MESSAGE_KIND, VIBEDATE_PRESENCE_KIND, allLeagueNames, canonicalHelloClaims, classifyHelloIdentity, conversationTag, createNostrPoolTransport, createNostrRelayLink, league, leagueIndex, leagueTopic, leaguesWithin, loadOrCreateIdentity, loadOrCreateNostrKey, loadPeers, matches, parseHandshake, parseTokensEnv, readUsage, recordPeer, recordPeerMessage, roomTopic, serializeHandshake, signHelloClaims, startDiscovery, startRoom, verifyHelloClaims };
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
DEMO_TOTAL_TOKENS,
|
|
6
6
|
LEAGUES,
|
|
7
7
|
LIVE_NOTICE,
|
|
8
|
+
ROOM_TOPIC_PREFIX,
|
|
8
9
|
TOKENS_ENV,
|
|
9
10
|
TOPIC_PREFIX,
|
|
10
11
|
VIBEDATE_MESSAGE_KIND,
|
|
@@ -29,11 +30,13 @@ import {
|
|
|
29
30
|
readUsage,
|
|
30
31
|
recordPeer,
|
|
31
32
|
recordPeerMessage,
|
|
33
|
+
roomTopic,
|
|
32
34
|
serializeHandshake,
|
|
33
35
|
signHelloClaims,
|
|
34
36
|
startDiscovery,
|
|
37
|
+
startRoom,
|
|
35
38
|
verifyHelloClaims
|
|
36
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-HDHJLZZG.js";
|
|
37
40
|
export {
|
|
38
41
|
BELOW_LEAGUE,
|
|
39
42
|
CANDIDATES,
|
|
@@ -41,6 +44,7 @@ export {
|
|
|
41
44
|
DEMO_TOTAL_TOKENS,
|
|
42
45
|
LEAGUES,
|
|
43
46
|
LIVE_NOTICE,
|
|
47
|
+
ROOM_TOPIC_PREFIX,
|
|
44
48
|
TOKENS_ENV,
|
|
45
49
|
TOPIC_PREFIX,
|
|
46
50
|
VIBEDATE_MESSAGE_KIND,
|
|
@@ -65,8 +69,10 @@ export {
|
|
|
65
69
|
readUsage,
|
|
66
70
|
recordPeer,
|
|
67
71
|
recordPeerMessage,
|
|
72
|
+
roomTopic,
|
|
68
73
|
serializeHandshake,
|
|
69
74
|
signHelloClaims,
|
|
70
75
|
startDiscovery,
|
|
76
|
+
startRoom,
|
|
71
77
|
verifyHelloClaims
|
|
72
78
|
};
|
package/dist/mcp.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibedate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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",
|