@easysociable/cli 0.1.4 → 0.1.5

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.
@@ -379,7 +379,7 @@ def _parse_compact_count(raw: str) -> str:
379
379
  def _fill_engagement_from_blob(out: dict[str, str], blob: str) -> None:
380
380
  if not blob:
381
381
  return
382
- # Prefer kind-first matches (discovery + list form), then num-first (capture prose).
382
+ # Prefer kind-first matches (list form), then num-first (capture prose).
383
383
  for kind, num in ENGAGEMENT_KIND_NUM_RE.findall(blob):
384
384
  key = f"src_{_normalize_metric_kind(kind)}"
385
385
  if key in out and not out[key]:
@@ -395,10 +395,10 @@ def _fill_engagement_from_blob(out: dict[str, str], blob: str) -> None:
395
395
 
396
396
 
397
397
  def parse_engagement_snapshot(text: str) -> dict[str, str]:
398
- """Parse engagement into src_* fields from Discovery or Capture/Raw notes.
398
+ """Parse engagement into src_* fields from Capture / Raw notes.
399
399
 
400
400
  Supports:
401
- - Discovery: `engagement snapshot: likes 113 / replies 18 / ... @ 2026-08-30`
401
+ - Compact: `engagement snapshot: likes 113 / replies 18 / ... @ 2026-08-30`
402
402
  - Capture prose: `Engagement snapshot (fetch DATE): ~92.9k views · 1086 likes · ...`
403
403
  - Raw list under `## Engagement snapshot` with `- likes: 3`
404
404
  Parenthetical secondary demos after the primary snapshot are ignored.
@@ -684,6 +684,7 @@ def build_graph() -> dict:
684
684
  "need_ids",
685
685
  "product_ids",
686
686
  "product_fit",
687
+ "recommendation_ids",
687
688
  "run",
688
689
  )
689
690
  if topic_rows:
@@ -801,6 +802,7 @@ def build_graph() -> dict:
801
802
  "need_ids": row.get("need_ids", "") or "",
802
803
  "product_ids": row.get("product_ids", "") or "",
803
804
  "product_fit": row.get("product_fit", "") or "",
805
+ "recommendation_ids": row.get("recommendation_ids", "") or "",
804
806
  "run_id": row["run"].strip(),
805
807
  "usage": usage,
806
808
  "src_heat_note": "来源帖热度,不是你的帖",
@@ -811,6 +813,13 @@ def build_graph() -> dict:
811
813
  edge(nid("topic", ident), nid("capture", row["capture_id"]), "from_capture")
812
814
  if row["run"].strip().startswith("RUN-"):
813
815
  edge(nid("topic", ident), nid("run", row["run"].strip()), "opened_run")
816
+ # Index cells are enough for edges; item frontmatter may repeat the same ids.
817
+ for need_id in re.findall(r"\bN-\d{8}-\d{2}\b", row.get("need_ids", "") or ""):
818
+ edge(nid("topic", ident), nid("need", need_id), "from_need")
819
+ for product_id in PRODUCT_RE.findall(row.get("product_ids", "") or ""):
820
+ edge(nid("topic", ident), nid("product", product_id), "uses_product")
821
+ for recommendation_id in RECOMMENDATION_RE.findall(row.get("recommendation_ids", "") or ""):
822
+ edge(nid("topic", ident), nid("recommendation", recommendation_id), "reviews")
814
823
  if item and (ROOT / item).exists():
815
824
  body = read(item)
816
825
  fm = re.match(r"^---\n(.*?)\n---", body, re.S)
@@ -1238,7 +1247,7 @@ def build_graph() -> dict:
1238
1247
 
1239
1248
 
1240
1249
  def _self_check() -> None:
1241
- # ponytail: tiny parser asserts; expand if capture/discovery formats drift again.
1250
+ # ponytail: tiny parser asserts; expand if capture/raw formats drift again.
1242
1251
  d = parse_engagement_snapshot(
1243
1252
  "- **engagement snapshot:** likes 113 / replies 18 / bookmarks 17 / views 9574 @ 2026-08-30\n"
1244
1253
  )
@@ -648,6 +648,11 @@
648
648
  gap_needs_n: "需求原话未选题 {n}",
649
649
  gap_lib_n: "Capture 未入库 {n}",
650
650
  gap_review_n: "已发但无法复盘 {n}",
651
+ topics_lead: "选题必须有明确输入:need / inbox / user / product / recommendation。没有 Discovery 中间层;来源热度不是选题。",
652
+ kv_lane: "Lane",
653
+ kv_mode: "生成路径",
654
+ kv_needs: "需求",
655
+ kv_recs: "推荐对象",
651
656
  kv_profile: "Profile",
652
657
  kind_profile: "Profile", kind_account: "账号",
653
658
  profile_pillars: "支柱",
@@ -764,6 +769,11 @@
764
769
  gap_needs_n: "{n} needs not yet in a topic",
765
770
  gap_lib_n: "{n} captures not ingested",
766
771
  gap_review_n: "{n} shipped posts not reviewable",
772
+ topics_lead: "Topics need an explicit input: need, inbox, user, product, or recommendation. No Discovery layer; source heat is not a topic.",
773
+ kv_lane: "Lane",
774
+ kv_mode: "Generation mode",
775
+ kv_needs: "Needs",
776
+ kv_recs: "Recommendations",
767
777
  kv_profile: "Profile",
768
778
  kind_profile: "Profile", kind_account: "Account",
769
779
  profile_pillars: "Pillars",
@@ -946,9 +956,8 @@
946
956
  let topicPillar = "";
947
957
  let topicSrc = "";
948
958
  let topicUsage = "";
949
- let discStatus = "";
950
- let discSort = "recommend";
951
959
  let capIngest = "";
960
+ const TOPIC_SOURCES = ["need", "inbox", "user", "product", "recommendation"];
952
961
  let selected = null;
953
962
  const $list = document.getElementById("list");
954
963
  const $insp = document.getElementById("insp");
@@ -1353,7 +1362,7 @@
1353
1362
  function qmatch(n) {
1354
1363
  const q = $q.value.trim().toLowerCase();
1355
1364
  if (!q) return true;
1356
- const blob = [n.ident, n.label, n.kind, n.status, n.pillar, n.lane, n.generation_mode, n.profile, n.profiles, n.src_handle, n.url, n.notes, n.type, n.atom_type, n.topic_id, n.audience, n.product_ids, n.product_fit, n.recommendation_ids].filter(Boolean).join(" ").toLowerCase();
1365
+ const blob = [n.ident, n.label, n.kind, n.status, n.pillar, n.lane, n.generation_mode, n.profile, n.profiles, n.src_handle, n.url, n.notes, n.type, n.atom_type, n.topic_id, n.audience, n.need_ids, n.product_ids, n.product_fit, n.recommendation_ids, n.source_type, n.capture_id].filter(Boolean).join(" ").toLowerCase();
1357
1366
  return blob.includes(q);
1358
1367
  }
1359
1368
  function connected(id) {
@@ -1454,7 +1463,7 @@
1454
1463
  }).join("");
1455
1464
  return `<div class="heat" title="${esc(t("heat_hint"))}">${chips}</div>${hint}`;
1456
1465
  }
1457
- function discoveryRowEl(n, extra) {
1466
+ function listRowEl(n, extra) {
1458
1467
  const d = document.createElement("div");
1459
1468
  d.className = "row" + (selected === n.id ? " sel" : "");
1460
1469
  d.dataset.kind = n.kind;
@@ -1473,46 +1482,6 @@
1473
1482
  </div>`;
1474
1483
  return d;
1475
1484
  }
1476
- function angleRecommendRank(n) {
1477
- const r = String(n.angle_recommend || "").toLowerCase();
1478
- if (r === "promote") return 0;
1479
- if (r === "library") return 1;
1480
- if (r === "hold") return 2;
1481
- if (r === "reject") return 3;
1482
- if (String(n.next_action || "") === "promote_to_topic") return 0;
1483
- return 4;
1484
- }
1485
- function discoveryUnprocessed(n) {
1486
- const st = String(n.status || "");
1487
- if (st === "promoted" || st === "rejected") return false;
1488
- const ang = String(n.angle_status || "");
1489
- if (ang === "accepted") return false;
1490
- if (ang === "held" || ang === "rejected") return false;
1491
- if (ang === "proposed" && (n.angle_recommend === "promote" || !n.angle_recommend)) return true;
1492
- if (String(n.next_action || "") === "promote_to_topic") return true;
1493
- return false;
1494
- }
1495
- function sortDiscoveries(rows, mode) {
1496
- const missing = -1;
1497
- const num = (n, key) => {
1498
- const v = Number(String(n[`src_${key}`] ?? "").replace(/,/g, ""));
1499
- return Number.isFinite(v) ? v : missing;
1500
- };
1501
- return rows.slice().sort((a, b) => {
1502
- if (mode === "recommend" || !mode) {
1503
- const rec = angleRecommendRank(a) - angleRecommendRank(b);
1504
- if (rec !== 0) return rec;
1505
- }
1506
- let cmp = 0;
1507
- if (mode === "likes") cmp = num(b, "likes") - num(a, "likes");
1508
- else if (mode === "replies") cmp = num(b, "replies") - num(a, "replies");
1509
- else if (mode === "views") cmp = num(b, "views") - num(a, "views");
1510
- else if (mode === "date") cmp = String(b.date || "").localeCompare(String(a.date || ""));
1511
- else cmp = num(b, "bookmarks") - num(a, "bookmarks");
1512
- if (cmp !== 0) return cmp;
1513
- return String(b.date || "").localeCompare(String(a.date || ""));
1514
- });
1515
- }
1516
1485
  function pills(n) {
1517
1486
  const bits = [];
1518
1487
  if (n.kind === "need") {
@@ -1522,18 +1491,10 @@
1522
1491
  if (n.source_type) bits.push(`<span class="pill">${esc(n.source_type)}</span>`);
1523
1492
  if (n.frequency) bits.push(`<span class="pill">×${esc(n.frequency)}</span>`);
1524
1493
  }
1525
- if (n.kind === "discovery") {
1526
- const st = n.status || "";
1527
- const cls = st === "read" || st === "promoted" ? "ok"
1528
- : (st === "saved" || st === "rejected" || st === "unread" ? "gap" : "");
1529
- if (st) bits.push(`<span class="pill ${cls}">${esc(st)}</span>`);
1530
- if (n.angle_recommend) bits.push(`<span class="pill ${n.angle_recommend === "promote" && discoveryUnprocessed(n) ? "gap" : ""}">${esc(n.angle_recommend)}</span>`);
1531
- else if (discoveryUnprocessed(n)) bits.push(`<span class="pill gap">${esc(t("gap_angle"))}</span>`);
1532
- if (n.source_role) bits.push(`<span class="pill">${esc(n.source_role)}</span>`);
1533
- if (n.next_action && n.next_action !== "none") bits.push(`<span class="pill">${esc(n.next_action)}</span>`);
1534
- }
1535
1494
  if (n.kind === "topic") {
1536
1495
  bits.push(`<span class="pill">${esc(n.status)}</span>`);
1496
+ if (n.source_type) bits.push(`<span class="pill">${esc(n.source_type)}</span>`);
1497
+ if (n.generation_mode) bits.push(`<span class="pill">${esc(n.generation_mode)}</span>`);
1537
1498
  bits.push(n.usage === "unused" ? `<span class="pill gap">${t("unused")}</span>` : `<span class="pill ok">${esc(n.usage)}</span>`);
1538
1499
  }
1539
1500
  if (n.kind === "run") {
@@ -1594,7 +1555,7 @@
1594
1555
  n.frequency ? `×${n.frequency}` : "",
1595
1556
  n.topic_ids && n.topic_ids !== "none" ? n.topic_ids : "",
1596
1557
  ].filter(Boolean).join(" · ");
1597
- cards.appendChild(discoveryRowEl(n, meta));
1558
+ cards.appendChild(listRowEl(n, meta));
1598
1559
  });
1599
1560
  if (!needs.length) {
1600
1561
  cards.innerHTML = `<p class='empty'>${t("no_need")}</p><p class="note">${esc(t("need_empty_hint"))}</p>`;
@@ -1709,58 +1670,31 @@
1709
1670
  onReady(wrap);
1710
1671
  }
1711
1672
 
1712
- function renderDiscovery() {
1713
- $list.innerHTML = "";
1714
- const statuses = [...new Set(nodesOf("discovery").map(x => x.status).filter(Boolean))];
1715
- const sortOpts = [
1716
- ["recommend", "f_sort_recommend"],
1717
- ["bookmarks", "f_sort_bookmarks"],
1718
- ["replies", "f_sort_replies"],
1719
- ["likes", "f_sort_likes"],
1720
- ["views", "f_sort_views"],
1721
- ["date", "f_sort_date"],
1722
- ];
1723
- filterBar(
1724
- `<select id="d-st"><option value="">${t("f_status")}</option>${statuses.map(s => `<option value="${s}" ${s === discStatus ? "selected" : ""}>${esc(s)}</option>`).join("")}</select>
1725
- <select id="d-sort" title="${esc(t("f_sort"))}"><option value="" disabled>${esc(t("f_sort"))}</option>${sortOpts.map(([v, k]) => `<option value="${v}" ${v === discSort ? "selected" : ""}>${esc(t(k))}</option>`).join("")}</select>`,
1726
- (w) => {
1727
- const status = w.querySelector("#d-st");
1728
- const sortEl = w.querySelector("#d-sort");
1729
- const paint = () => {
1730
- discStatus = status.value;
1731
- discSort = sortEl.value || "recommend";
1732
- const cards = document.createElement("div");
1733
- cards.className = "cards";
1734
- let rows = nodesOf("discovery").filter(qmatch);
1735
- if (discStatus) rows = rows.filter(x => x.status === discStatus);
1736
- rows = sortDiscoveries(rows, discSort);
1737
- rows.forEach(x => {
1738
- const meta = [
1739
- x.angle_recommend || "",
1740
- x.source_role,
1741
- x.next_action && x.next_action !== "none" ? x.next_action : "",
1742
- `@${x.source_handle || "—"}`,
1743
- ].filter(Boolean).join(" · ");
1744
- cards.appendChild(discoveryRowEl(x, meta));
1745
- });
1746
- if (!rows.length) cards.innerHTML = `<p class='empty'>${t("no_discovery")}</p>`;
1747
- [...$list.querySelectorAll(".cards")].forEach(n => n.remove());
1748
- $list.appendChild(cards);
1749
- };
1750
- status.onchange = paint;
1751
- sortEl.onchange = paint;
1752
- paint();
1753
- }
1754
- );
1673
+ function topicMeta(x) {
1674
+ const score = x.fit ? `${x.craft}/${x.fit}` : (x.craft || x.total || "—");
1675
+ const bits = [
1676
+ score,
1677
+ formatPillar(x.pillar),
1678
+ x.lane,
1679
+ x.generation_mode,
1680
+ x.source_type,
1681
+ x.need_ids && x.need_ids !== "none" ? x.need_ids : "",
1682
+ x.product_ids && x.product_ids !== "none" ? x.product_ids : "",
1683
+ x.recommendation_ids && x.recommendation_ids !== "none" ? x.recommendation_ids : "",
1684
+ x.capture_id || "",
1685
+ ].filter(Boolean);
1686
+ return bits.join(" · ");
1755
1687
  }
1756
1688
 
1757
1689
  function renderTopics() {
1758
1690
  $list.innerHTML = "";
1691
+ $list.insertAdjacentHTML("beforeend", `<p class="lead">${esc(t("topics_lead"))}</p>`);
1692
+ if (topicSrc && !TOPIC_SOURCES.includes(topicSrc)) topicSrc = "";
1759
1693
  const statuses = [...new Set(nodesOf("topic").map(x => x.status).filter(Boolean))];
1760
1694
  filterBar(
1761
1695
  `<select id="f-st"><option value="">${t("f_status")}</option>${statuses.map(s => `<option value="${s}" ${s === topicStatus ? "selected" : ""}>${esc(s)}</option>`).join("")}</select>
1762
1696
  <select id="f-p"><option value="">${t("f_pillar")}</option>${pillarIds().map(id => `<option value="${id}" ${id === topicPillar ? "selected" : ""}>${esc(formatPillar(id))}</option>`).join("")}</select>
1763
- <select id="f-src"><option value="">${t("f_src")}</option>${["watchlist", "inbox", "user", "discovery", "need"].map(s => `<option value="${s}" ${s === topicSrc ? "selected" : ""}>${s}</option>`).join("")}</select>
1697
+ <select id="f-src"><option value="">${t("f_src")}</option>${TOPIC_SOURCES.map(s => `<option value="${s}" ${s === topicSrc ? "selected" : ""}>${s}</option>`).join("")}</select>
1764
1698
  <select id="f-u"><option value="">${t("f_use")}</option>
1765
1699
  <option value="unused" ${topicUsage === "unused" ? "selected" : ""}>${t("f_unused")}</option>
1766
1700
  <option value="promoted" ${topicUsage === "promoted" ? "selected" : ""}>${t("f_promoted")}</option></select>`,
@@ -1783,8 +1717,7 @@
1783
1717
  if (topicSrc) rows = rows.filter(x => x.source_type === topicSrc);
1784
1718
  if (topicUsage) rows = rows.filter(x => x.usage === topicUsage);
1785
1719
  rows.forEach(x => {
1786
- const score = x.fit ? `${x.craft}/${x.fit}` : (x.craft || x.total || "—");
1787
- cards.appendChild(rowEl(x, `${score} · ${formatPillar(x.pillar)} · ${x.source_type} @${x.src_handle || "—"}`));
1720
+ cards.appendChild(rowEl(x, topicMeta(x)));
1788
1721
  });
1789
1722
  if (!rows.length) cards.innerHTML = `<p class='empty'>${t("no_topic")}</p>`;
1790
1723
  $list.appendChild(cards);
@@ -1826,7 +1759,7 @@
1826
1759
  let caps = nodesOf("capture").filter(qmatch).sort((a, b) => (b.date || "").localeCompare(a.date || ""));
1827
1760
  if (capIngest === "ingested") caps = caps.filter(c => c.status === "ingested");
1828
1761
  if (capIngest === "not_ingested") caps = caps.filter(c => c.status !== "ingested");
1829
- caps.forEach(c => cards.appendChild(discoveryRowEl(c, `${c.type} · ${c.status} · raw ${c.has_raw ? t("raw_yes") : t("raw_no")}`)));
1762
+ caps.forEach(c => cards.appendChild(listRowEl(c, `${c.type} · ${c.status} · raw ${c.has_raw ? t("raw_yes") : t("raw_no")}`)));
1830
1763
  if (!caps.length) cards.innerHTML = `<p class='empty'>${t("no_note")}</p>`;
1831
1764
  $list.appendChild(cards);
1832
1765
  return;
@@ -2047,20 +1980,18 @@
2047
1980
 
2048
1981
  function gapsFor(profileId) {
2049
1982
  const topics = profileId ? nodesOfAll("topic").filter(n => n.profile === profileId) : nodesOfAll("topic");
2050
- const discs = profileId ? nodesOfAll("discovery").filter(n => n.profile === profileId) : nodesOfAll("discovery");
2051
1983
  const needs = profileId ? nodesOfAll("need").filter(n => n.profile === profileId) : nodesOfAll("need");
2052
1984
  const caps = profileId ? nodesOfAll("capture").filter(n => n.profile === profileId) : nodesOfAll("capture");
2053
1985
  const pubs = profileId ? nodesOfAll("published").filter(n => n.profile === profileId) : nodesOfAll("published");
2054
1986
  const needCount = needs.length;
2055
1987
  const gaps = [
2056
1988
  [topics.filter(x => x.status === "ready" && x.usage === "unused").length, "gap_ready_n"],
2057
- [discs.filter(discoveryUnprocessed).length, "gap_angle_n"],
2058
1989
  [needs.filter(n => n.status === "captured" && (!n.topic_ids || n.topic_ids === "none")).length, "gap_needs_n"],
2059
1990
  [caps.filter(c => c.status !== "ingested").length, "gap_lib_n"],
2060
1991
  [pubs.filter(p => !p.reviewable).length, "gap_review_n"],
2061
1992
  ].filter(([n]) => n > 0);
2062
1993
  if (GRAPH.needs_index_present && needCount < 3) {
2063
- gaps.splice(2, 0, [needCount, "gap_needs_thin_n"]);
1994
+ gaps.splice(1, 0, [needCount, "gap_needs_thin_n"]);
2064
1995
  }
2065
1996
  return gaps;
2066
1997
  }
@@ -2166,20 +2097,21 @@
2166
2097
  ? `${n.win || 0}w / ${n.loss || 0}l / n=${n.n}`
2167
2098
  : "",
2168
2099
  [t("kv_pillar")]: formatPillar(n.pillar),
2169
- Lane: n.kind === "topic" ? (n.lane || "") : "",
2170
- "Generation mode": n.kind === "topic" ? (n.generation_mode || "") : "",
2100
+ [t("kv_lane")]: n.kind === "topic" ? (n.lane || "") : "",
2101
+ [t("kv_mode")]: n.kind === "topic" ? (n.generation_mode || "") : "",
2171
2102
  [t("kv_plat")]: n.platform || n.platforms,
2172
2103
  [t("kv_profile")]: n.profile || n.profile_id || (n.profiles || ""),
2173
2104
  [t("kv_score")]: n.fit ? `${n.craft} opp / ${n.fit} aud` : n.total,
2174
- [t("kv_use")]: n.kind === "discovery" ? (n.angle_recommend || n.next_action || "") : n.usage,
2175
- [t("kv_src")]: n.kind === "discovery" ? (n.source_role || "") : n.source_type,
2176
- Angle: n.kind === "discovery" ? ([n.angle_status, n.need_ids].filter(Boolean).join(" · ")) : "",
2105
+ [t("kv_use")]: n.usage,
2106
+ [t("kv_src")]: n.source_type,
2107
+ [t("kv_needs")]: n.kind === "topic" ? (n.need_ids || "") : "",
2177
2108
  Quote: n.kind === "need" ? (n.quote || "") : "",
2178
2109
  Speaker: n.kind === "need" ? (n.speaker || "") : "",
2179
2110
  Frequency: n.kind === "need" ? (n.frequency || "") : "",
2180
2111
  Topics: n.kind === "need" ? (n.topic_ids || "") : "",
2181
2112
  Products: n.kind === "topic" ? (n.product_ids || "") : "",
2182
2113
  "Product fit": n.kind === "topic" ? (n.product_fit || "") : "",
2114
+ [t("kv_recs")]: n.kind === "topic" ? (n.recommendation_ids || "") : "",
2183
2115
  Audience: n.kind === "product" ? (n.audience || "") : "",
2184
2116
  Price: n.kind === "product" ? (n.price || "") : "",
2185
2117
  "Effective from": n.kind === "product" ? (n.effective_from || "") : "",
@@ -2187,10 +2119,10 @@
2187
2119
  Relationship: n.kind === "recommendation" ? (n.relationship || "") : "",
2188
2120
  "Last researched": n.kind === "recommendation" ? (n.last_researched || "") : "",
2189
2121
  Handle: n.source_handle || n.src_handle || "",
2190
- [t("kv_heat_at")]: (n.kind === "discovery" || n.kind === "capture") ? (n.engagement_at || "") : "",
2122
+ [t("kv_heat_at")]: n.kind === "capture" ? (n.engagement_at || "") : "",
2191
2123
  [t("kv_result")]: n.result,
2192
2124
  }).filter(([, v]) => v);
2193
- const heatBlock = (n.kind === "discovery" || n.kind === "capture")
2125
+ const heatBlock = n.kind === "capture"
2194
2126
  ? `${heatStripHtml(n, { hint: true })}${n.source_url ? `<p class="note">${t("src_url")}<a href="${esc(n.source_url)}" target="_blank" rel="noreferrer">${esc(n.source_url)}</a></p>` : ""}`
2195
2127
  : "";
2196
2128
  $insp.innerHTML = `<h2>${t("obj")}</h2>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easysociable/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Agent-first CLI for EasySociable: Content Engine, Template, Slideshow, and MCP",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -49,9 +49,9 @@
49
49
  "tsup": "^8.0.2",
50
50
  "typescript": "^5.8.3",
51
51
  "vitest": "^1.3.1",
52
+ "@easysociable/template-gateway-client": "0.0.0",
52
53
  "@easysociable/mcp-server": "0.0.0",
53
54
  "@easysociable/tsconfig": "0.0.0",
54
- "@easysociable/template-gateway-client": "0.0.0",
55
55
  "@easysociable/vitest-config": "0.0.0"
56
56
  },
57
57
  "scripts": {