@decentnetwork/beagle 0.1.6 → 0.1.8

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/call-ice.js CHANGED
@@ -1,13 +1,10 @@
1
1
  // Carrier bootstrap TURN credentials for browser WebRTC.
2
2
  //
3
3
  // Android online calls use CarrierExtension.getTurnServerInfo() →
4
- // carrier_get_turn_server(). Prefer @decentnetwork/peer getIceServers() when
5
- // the installed peer version exports it; otherwise fall back to
6
- // deriveCarrierTurnCreds via the package dist path (older peer builds).
4
+ // carrier_get_turn_server(). We call @decentnetwork/peer getIceServers()
5
+ // (0.1.136+) with the same credential scheme.
7
6
  import { readFileSync, existsSync } from "node:fs";
8
- import { dirname, join } from "node:path";
9
- import { fileURLToPath, pathToFileURL } from "node:url";
10
- import { base58ToBytes, carrierIdFromPublicKey } from "@decentnetwork/peer";
7
+ import { carrierIdFromPublicKey, getIceServers } from "@decentnetwork/peer";
11
8
  function hexToBytes(hex) {
12
9
  const clean = hex.trim().replace(/^0x/i, "");
13
10
  if (clean.length % 2 !== 0)
@@ -38,29 +35,11 @@ export async function buildCarrierCallIceServers(opts) {
38
35
  if (!nodes.length)
39
36
  throw new Error("no bootstrap nodes for TURN");
40
37
  const { publicKey, secretKey } = loadKeyPair(opts.keyFile);
41
- const userid = carrierIdFromPublicKey(publicKey);
42
- const peerRoot = dirname(fileURLToPath(import.meta.resolve("@decentnetwork/peer")));
43
- const peerMod = (await import(pathToFileURL(join(peerRoot, "index.js")).href));
44
- if (typeof peerMod.getIceServers === "function") {
45
- return peerMod.getIceServers({
46
- bootstrapNodes: nodes,
47
- ourUserid: userid,
48
- ourSecretKey: secretKey,
49
- limit: opts.limit ?? 3,
50
- includeTcpTurn: true,
51
- });
52
- }
53
- const { deriveCarrierTurnCreds } = (await import(pathToFileURL(join(peerRoot, "turn-creds.js")).href));
54
- const iceServers = [];
55
- for (const n of nodes) {
56
- const creds = deriveCarrierTurnCreds({
57
- bootnodeHost: n.host,
58
- bootnodePublicKey: base58ToBytes(n.pk),
59
- ourUserid: userid,
60
- ourSecretKey: secretKey,
61
- });
62
- const base = `${creds.host}:${creds.port}`;
63
- iceServers.push({ urls: `stun:${base}`, username: creds.username, credential: creds.password }, { urls: `turn:${base}`, username: creds.username, credential: creds.password }, { urls: `turn:${base}?transport=tcp`, username: creds.username, credential: creds.password });
64
- }
65
- return iceServers;
38
+ return getIceServers({
39
+ bootstrapNodes: nodes,
40
+ ourUserid: carrierIdFromPublicKey(publicKey),
41
+ ourSecretKey: secretKey,
42
+ limit: opts.limit ?? 3,
43
+ includeTcpTurn: true,
44
+ });
66
45
  }
@@ -1,4 +1,4 @@
1
- window.__DK_UI_VERSION="0.1.6";
1
+ window.__DK_UI_VERSION="0.1.7";
2
2
  const ICON_PATHS = {
3
3
  // ---- tab bar (the four must feel like one set) ----
4
4
  users: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
@@ -1297,7 +1297,7 @@ function PeerRow({ peer, T, active, onClick }) {
1297
1297
  textOverflow: "ellipsis",
1298
1298
  flex: 1,
1299
1299
  minWidth: 0
1300
- } }, peer.lastMsg))), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 4, flexShrink: 0 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10.5, color: "var(--faint)" } }, peer.lastTime), peer.unread ? /* @__PURE__ */ React.createElement(Unread, { n: peer.unread }) : /* @__PURE__ */ React.createElement(StatusDot, { online: peer.online })));
1300
+ } }, peer.lastMsg))), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 4, flexShrink: 0 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10.5, color: "var(--faint)" } }, peer.lastTime), peer.unread ? /* @__PURE__ */ React.createElement(Unread, { n: peer.unread }) : peer.pending ? /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 11, stroke: 2.2, color: "var(--warn, #d29922)", title: "friend request pending" }) : /* @__PURE__ */ React.createElement(StatusDot, { online: peer.online })));
1301
1301
  }
1302
1302
  function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed }) {
1303
1303
  const [q, setQ] = React.useState("");
@@ -1964,7 +1964,15 @@ function Msg({ m, peer, T, onTheater, onDelete, onCancel, onRetry, onReveal, onC
1964
1964
  style: { background: "none", border: "none", cursor: "pointer", padding: 0, marginLeft: 2, display: "inline-flex", opacity: 0.55 }
1965
1965
  },
1966
1966
  /* @__PURE__ */ React.createElement(Icon, { name: "trash", size: 11, stroke: 2, color: "var(--faint)" })
1967
- ), mine && m.status === "queued" ? /* @__PURE__ */ React.createElement("span", { style: { display: "inline-flex", alignItems: "center", gap: 2 }, title: "waiting for peer \u2014 will send when they're online" }, /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 11, stroke: 2.2, color: "var(--faint)" }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10, color: "var(--faint)" } }, T.queued || "queued")) : mine && m.status && /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 12, stroke: 2.2, color: m.status === "read" ? "var(--accent)" : "var(--faint)" })))
1967
+ ), mine && (m.status === "queued" || m.status === "sending") ? /* @__PURE__ */ React.createElement(
1968
+ "span",
1969
+ {
1970
+ style: { display: "inline-flex", alignItems: "center", gap: 2 },
1971
+ title: m.status === "sending" ? "delivering\u2026" : "waiting for peer \u2014 will send when they're online"
1972
+ },
1973
+ /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 11, stroke: 2.2, color: "var(--faint)" }),
1974
+ /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10, color: "var(--faint)" } }, m.status === "sending" ? T.sendingMsg || "sending" : T.queued || "queued")
1975
+ ) : mine && m.status && /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 12, stroke: 2.2, color: m.status === "read" ? "var(--accent)" : "var(--faint)" })))
1968
1976
  );
1969
1977
  }
1970
1978
  function Conversation({ T, peer, lang, thread: threadProp, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread }) {
@@ -2253,6 +2261,17 @@ ${peer.address}`
2253
2261
  setMenu(false);
2254
2262
  onRemove(peer);
2255
2263
  } }))))),
2264
+ peer.pending && /* @__PURE__ */ React.createElement("div", { style: {
2265
+ flexShrink: 0,
2266
+ padding: "7px 16px",
2267
+ display: "flex",
2268
+ alignItems: "center",
2269
+ gap: 8,
2270
+ background: "color-mix(in srgb, #d29922 12%, var(--panel))",
2271
+ borderBottom: "1px solid var(--line)",
2272
+ fontSize: 12.5,
2273
+ color: "var(--text)"
2274
+ } }, /* @__PURE__ */ React.createElement(Icon, { name: "clock", size: 13, stroke: 2.2, color: "#d29922" }), /* @__PURE__ */ React.createElement("span", null, T.pendingFriend || "Waiting for them to accept your friend request \u2014 messages will queue and deliver once they do.")),
2256
2275
  /* @__PURE__ */ React.createElement("div", { ref: scrollRef, onScroll: updateFollowScroll, style: { flex: 1, overflow: "auto", padding: "18px 22px" } }, /* @__PURE__ */ React.createElement("div", { style: { maxWidth: 760, margin: "0 auto" } }, thread.filter((m) => m.day || !hidden.has(m.id)).map((m, i) => m.day ? /* @__PURE__ */ React.createElement("div", { key: i, style: { display: "flex", justifyContent: "center", margin: "14px 0" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 10.5, fontWeight: 600, color: "var(--faint)", background: "var(--chip)", padding: "3px 10px", borderRadius: 999 } }, m.day)) : /* @__PURE__ */ React.createElement(
2257
2276
  Msg,
2258
2277
  {
@@ -3466,9 +3485,11 @@ const STR = {
3466
3485
  send: "Send",
3467
3486
  pickPeer: "Select a peer to open the conversation",
3468
3487
  queued: "queued",
3488
+ sendingMsg: "sending",
3469
3489
  open: "open",
3470
3490
  retry: "Retry",
3471
3491
  cancel: "Cancel",
3492
+ pendingFriend: "Waiting for them to accept your friend request \u2014 messages will queue and deliver once they do.",
3472
3493
  cancelling: "Cancelling\u2026",
3473
3494
  cancelled: "Transfer cancelled",
3474
3495
  retrying: "Retrying\u2026",
@@ -3560,9 +3581,11 @@ const STR = {
3560
3581
  send: "\u53D1\u9001",
3561
3582
  pickPeer: "\u9009\u62E9\u4E00\u4F4D\u597D\u53CB\u5F00\u59CB\u4F1A\u8BDD",
3562
3583
  queued: "\u5F85\u53D1\u9001",
3584
+ sendingMsg: "\u53D1\u9001\u4E2D",
3563
3585
  open: "\u6253\u5F00",
3564
3586
  retry: "\u91CD\u8BD5",
3565
3587
  cancel: "\u53D6\u6D88",
3588
+ pendingFriend: "\u7B49\u5F85\u5BF9\u65B9\u63A5\u53D7\u597D\u53CB\u8BF7\u6C42 \u2014 \u6D88\u606F\u4F1A\u5148\u6392\u961F\uFF0C\u5BF9\u65B9\u63A5\u53D7\u540E\u81EA\u52A8\u9001\u8FBE\u3002",
3566
3589
  cancelling: "\u6B63\u5728\u53D6\u6D88\u2026",
3567
3590
  cancelled: "\u5DF2\u53D6\u6D88\u4F20\u8F93",
3568
3591
  retrying: "\u6B63\u5728\u91CD\u8BD5\u2026",
@@ -47,6 +47,10 @@ export class EmbeddedHost {
47
47
  #logger = new Logger({ prefix: "Beagle" });
48
48
  #opts;
49
49
  #events = new EventEmitter();
50
+ /** Per-peer delivery chains: background sends to one peer run strictly in
51
+ * order, so a new message can never overtake an older one whose ACK is
52
+ * still pending. Entry removed when the chain drains. */
53
+ #sendChains = new Map();
50
54
  /** Friend requests held for manual accept when auto-accept is off. */
51
55
  #pending = new Map();
52
56
  /** Inbound call signals waiting for the UI's long-poll to collect them. */
@@ -126,8 +130,21 @@ export class EmbeddedHost {
126
130
  });
127
131
  this.#node.on("friend-connection", (e) => {
128
132
  this.#events.emit("event", { type: "presence", userid: e.pubkey, status: e.status });
129
- if (e.status === "connected")
130
- void this.#flushOutbox(e.pubkey);
133
+ // The connected event can arrive a beat before the session accepts
134
+ // traffic — observed live: flush at connect said "friend is offline",
135
+ // and nothing retried until the user's next send, so a queued message
136
+ // sat visibly stuck despite the peer being online. One delayed retry
137
+ // covers that gap; the per-send chain and later reconnects cover the
138
+ // rest.
139
+ if (e.status === "connected") {
140
+ void this.#flushOutbox(e.pubkey)
141
+ .catch(() => { })
142
+ .then(() => {
143
+ if (this.#messages.queuedOutgoing(e.pubkey).length === 0)
144
+ return;
145
+ return new Promise((r) => setTimeout(r, 4000)).then(() => this.#flushOutbox(e.pubkey).catch(() => { }));
146
+ });
147
+ }
131
148
  });
132
149
  this.#node.on("call-signal", (evt) => {
133
150
  this.#callQueue.push({ userid: evt.pubkey, data: Buffer.from(evt.data).toString("utf-8") });
@@ -308,6 +325,18 @@ export class EmbeddedHost {
308
325
  alias: meta?.alias,
309
326
  name: meta?.alias || f.name || f.carrierId,
310
327
  status: f.status,
328
+ // Shareable Carrier address — the SDK derives it when the
329
+ // record lacks one, so this is always populated (peer ≥0.1.138).
330
+ address: f.address,
331
+ // Friendship established vs merely requested. acceptedAt is the
332
+ // SDK's cryptographic proof (session established, or their
333
+ // DHT-PK announce); an inbound message is equally conclusive
334
+ // and covers records that predate acceptedAt. Without this the
335
+ // UI shows a live-looking chat while every send quietly queues
336
+ // (INBOX 2026-08-05: 13 messages, zero delivered, no clue why).
337
+ accepted: !!f.acceptedAt ||
338
+ f.status === "online" ||
339
+ this.#messages.hasInbound(f.carrierId),
311
340
  lastSeen: f.acceptedAt,
312
341
  pinned: meta?.pinned ?? false,
313
342
  lastMessage: lastMsg ? { dir: lastMsg.dir, text: lastMsg.text, ts: lastMsg.ts } : undefined,
@@ -368,27 +397,37 @@ export class EmbeddedHost {
368
397
  // user's own words vanish from their own thread — they watched the
369
398
  // peer answer a question that wasn't on screen (INBOX 2026-08-02).
370
399
  // A send failure is a delivery state ON the message, never its absence.
371
- const msg = this.#messages.append(uid, "out", text, Date.now());
400
+ const msg = this.#messages.append(uid, "out", text, Date.now(), "sending");
372
401
  this.#meta.ensure(uid);
373
402
  this.#events.emit("event", { type: "chat", userid: uid, dir: "out" });
374
- // Anything already queued for this peer goes firstthe new message
375
- // must not overtake older ones the moment the path recovers.
376
- await this.#flushOutbox(uid).catch(() => { });
377
- try {
378
- await this.#node.sendText(uid, text);
379
- this.#messages.setStatus(uid, msg.id, "sent");
380
- return {};
381
- }
382
- catch (e) {
383
- // Offline peer or failed delivery: same recovery either way — keep
384
- // the message "queued" (the UI shows the clock) and deliver it on
385
- // the friend's reconnect via #flushOutbox. This is a success from
386
- // the caller's side: the store owns the message; only its delivery
387
- // is pending.
388
- this.#logger.info(`send to ${uid.slice(0, 8)} queued: ${e.message}`);
389
- this.#messages.setStatus(uid, msg.id, "queued");
390
- return { queued: true };
391
- }
403
+ // Return NOW. The delivery attempt runs in the background an ACK
404
+ // that takes 15s per hop must cost the network 15s, never the user
405
+ // (INBOX 2026-08-05: a peer that never ACKed made every send block
406
+ // the UI for 30s, which reads as "stuck" and provokes re-sends).
407
+ // Per-peer chaining keeps FIFO: anything queued flushes first, and a
408
+ // second send waits for this one's attempt before its own.
409
+ const prev = this.#sendChains.get(uid) ?? Promise.resolve();
410
+ const job = prev.then(async () => {
411
+ await this.#flushOutbox(uid).catch(() => { });
412
+ try {
413
+ await this.#node.sendText(uid, text);
414
+ this.#messages.setStatus(uid, msg.id, "sent");
415
+ }
416
+ catch (e) {
417
+ // Offline peer or failed delivery: same recovery either way —
418
+ // "queued" (the UI shows the clock) and #flushOutbox delivers it
419
+ // on the friend's reconnect. A status update, not a failure.
420
+ this.#logger.info(`send to ${uid.slice(0, 8)} queued: ${e.message}`);
421
+ this.#messages.setStatus(uid, msg.id, "queued");
422
+ }
423
+ this.#events.emit("event", { type: "chat", userid: uid, dir: "out" });
424
+ });
425
+ this.#sendChains.set(uid, job);
426
+ void job.finally(() => {
427
+ if (this.#sendChains.get(uid) === job)
428
+ this.#sendChains.delete(uid);
429
+ });
430
+ return {};
392
431
  }
393
432
  case "chat-log-local":
394
433
  this.#messages.append(uid, req.dir ?? "in", String(req.text ?? ""), Date.now());
@@ -14,10 +14,12 @@ export interface ChatMessage {
14
14
  ts: number;
15
15
  /** Stable per-message id (ts + per-process sequence) for UI keys / dedup. */
16
16
  id: string;
17
- /** Outgoing-text delivery state. "queued" means the peer was offline when the
18
- * user hit send, so the daemon stored it and will deliver it (in order) the
19
- * moment the friend reconnects. Cleared once actually sent. */
20
- status?: "queued" | "sent" | "failed";
17
+ /** Outgoing-text delivery state. "sending" = the background delivery attempt
18
+ * is still running (chat-send returns before the network is consulted, so
19
+ * the UI never blocks on a slow peer). "queued" = that attempt gave up; the
20
+ * daemon will deliver it (in order) the moment the friend reconnects.
21
+ * Cleared once actually sent. */
22
+ status?: "sending" | "queued" | "sent" | "failed";
21
23
  /** How this message traversed the network, for the UI to distinguish at a
22
24
  * glance (different colors). "online" = live net_crypto session (direct/relay);
23
25
  * "offline" = express store-and-forward (the friend or we were offline). Lets
@@ -52,11 +54,15 @@ export declare class MessageStore {
52
54
  /** Append a message and schedule a flush. Returns the stored message.
53
55
  * Pass `status: "queued"` for an outgoing text the peer wasn't online to
54
56
  * receive — the daemon flushes it on reconnect. */
55
- append(peer: string, dir: "in" | "out", text: string, ts?: number, status?: "queued" | "sent" | "failed", via?: "online" | "offline"): ChatMessage;
57
+ append(peer: string, dir: "in" | "out", text: string, ts?: number, status?: "sending" | "queued" | "sent" | "failed", via?: "online" | "offline"): ChatMessage;
56
58
  /** Set (or, with undefined, clear) the delivery status on a text message.
57
59
  * Used to flip a "queued" message to delivered once it's actually sent.
58
60
  * No-op if the id isn't found or is a file entry. Returns true if patched. */
59
- setStatus(peer: string, id: string, status?: "queued" | "sent" | "failed"): boolean;
61
+ setStatus(peer: string, id: string, status?: "sending" | "queued" | "sent" | "failed"): boolean;
62
+ /** Has this peer ever sent us anything? Used as proof-of-friendship for
63
+ * records that predate the SDK's acceptedAt bookkeeping — a message can
64
+ * only arrive over an established session. */
65
+ hasInbound(peer: string): boolean;
60
66
  /** Outgoing messages still awaiting delivery (peer was offline), oldest
61
67
  * first — both queued text and queued file chips. The daemon drains this
62
68
  * on a friend's reconnect. */
@@ -33,6 +33,11 @@ export class MessageStore {
33
33
  let total = 0;
34
34
  for (const [peer, msgs] of Object.entries(raw)) {
35
35
  if (Array.isArray(msgs)) {
36
+ // A "sending" state cannot survive a restart — the attempt it
37
+ // described died with the process. Requeue so #flushOutbox owns it.
38
+ for (const m of msgs)
39
+ if (m.status === "sending")
40
+ m.status = "queued";
36
41
  this.byPeer.set(peer, msgs);
37
42
  total += msgs.length;
38
43
  }
@@ -69,6 +74,12 @@ export class MessageStore {
69
74
  this.scheduleSave();
70
75
  return true;
71
76
  }
77
+ /** Has this peer ever sent us anything? Used as proof-of-friendship for
78
+ * records that predate the SDK's acceptedAt bookkeeping — a message can
79
+ * only arrive over an established session. */
80
+ hasInbound(peer) {
81
+ return (this.byPeer.get(peer) ?? []).some((m) => m.dir === "in");
82
+ }
72
83
  /** Outgoing messages still awaiting delivery (peer was offline), oldest
73
84
  * first — both queued text and queued file chips. The daemon drains this
74
85
  * on a friend's reconnect. */
package/dist/server.js CHANGED
@@ -20,6 +20,7 @@ import { dirname, join } from "node:path";
20
20
  import { INSTALL_COMMAND as LAN_INSTALL_COMMAND } from "./lan-handoff.js";
21
21
  import yaml from "js-yaml";
22
22
  import { DEFAULT_EXITS } from "./exits.js";
23
+ import * as peerAddr from "@decentnetwork/peer";
23
24
  // Directory holding the built desktop UI bundle (index.html, app.js, vendor/).
24
25
  // scripts/build-ui.mjs emits it next to this compiled module at dist/ui/desktop/.
25
26
  const DESKTOP_DIR = join(dirname(fileURLToPath(import.meta.url)), "desktop");
@@ -690,6 +691,11 @@ export function startBeagleServer(opts) {
690
691
  // another friend (userid only works once already friends).
691
692
  address: f.address || "",
692
693
  online,
694
+ // Friendship not yet accepted by the other side. Only trusted when
695
+ // the backend states it explicitly (embedded does); a daemon
696
+ // backend that doesn't report it defaults to established, so the
697
+ // banner can never wrongly appear on an old backend.
698
+ pending: f.accepted === false,
693
699
  via: online ? viaFromTransport(sess?.transport) : null,
694
700
  ping: null,
695
701
  ip: ipByUserid.get(uid) ?? "",
@@ -965,7 +971,18 @@ export function startBeagleServer(opts) {
965
971
  return;
966
972
  }
967
973
  if (req.method === "POST" && url === "/api/add") {
968
- const { address } = await readBody(req);
974
+ let { address } = await readBody(req);
975
+ // Accept a 44-char userid too: the address is userid's pubkey plus a
976
+ // derivable suffix (nospam=0 network-wide + checksum), and users
977
+ // frequently have only the userid — it's what every other surface
978
+ // shows. Deriving here beats rejecting with "not an address".
979
+ try {
980
+ const raw = String(address ?? "").trim();
981
+ const bytes = peerAddr.base58ToBytes(raw);
982
+ if (bytes.length === 32)
983
+ address = peerAddr.carrierAddressFromPublicKey(bytes, 0);
984
+ }
985
+ catch { /* not base58 — let the backend judge it as-is */ }
969
986
  // A friend-request is fire-and-forget: it's delivered when the peer is
970
987
  // online and only becomes a friend once they accept. Don't let the UI
971
988
  // hang on the network round-trip (DHT lookup / unreachable peer) — kick
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@decentnetwork/beagle",
3
- "version": "0.1.6",
4
- "description": "Beagle \u2014 P2P chat, file transfer and calls for regular users, on the Decent Network. No admin privilege required.",
3
+ "version": "0.1.8",
4
+ "description": "Beagle P2P chat, file transfer and calls for regular users, on the Decent Network. No admin privilege required.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "bin": {
@@ -27,7 +27,7 @@
27
27
  "dependencies": {
28
28
  "@decentnetwork/chat-components": "^0.1.3",
29
29
  "@decentnetwork/lan": "^0.1.256",
30
- "@decentnetwork/peer": "^0.1.136",
30
+ "@decentnetwork/peer": "^0.1.138",
31
31
  "@decentnetwork/peer-webrtc": "^0.2.10",
32
32
  "js-yaml": "^4.1.0",
33
33
  "yargs": "^17.7.2"