birdclaw 0.8.4 → 0.9.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.
Files changed (116) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +22 -2
  3. package/dist/cli/birdclaw.js +1233 -192
  4. package/dist/client/assets/AvatarChip-C8zELEVt.js +1 -0
  5. package/dist/client/assets/ProfilePreview-CsiLpFrb.js +1 -0
  6. package/dist/client/assets/SavedTimelineView-DE4CXK-h.js +1 -0
  7. package/dist/client/assets/SmartTimestamp-BypN4KPq.js +1 -0
  8. package/dist/client/assets/TimelineRouteFrame-D6djyMAT.js +1 -0
  9. package/dist/client/assets/blocks-h27nF3TL.js +1 -0
  10. package/dist/client/assets/{bookmarks-CbBkMvXk.js → bookmarks-Cprrli-V.js} +1 -1
  11. package/dist/client/assets/circle-check-BCK_qvXt.js +1 -0
  12. package/dist/client/assets/client-stream-contracts-D1QUdWfh.js +5 -0
  13. package/dist/client/assets/createLucideIcon-CMLcIxOz.js +1 -0
  14. package/dist/client/assets/data-sources-Beb3JZkl.js +1 -0
  15. package/dist/client/assets/discuss-ciwVz7Fo.js +1 -0
  16. package/dist/client/assets/dms-BZ3GUPey.js +1 -0
  17. package/dist/client/assets/inbox-Dajdut79.js +1 -0
  18. package/dist/client/assets/index-a1ZAcWSa.js +31 -0
  19. package/dist/client/assets/likes-CxZZqiZC.js +1 -0
  20. package/dist/client/assets/links-CsWS9uKL.js +1 -0
  21. package/dist/client/assets/mapbox-dx7h2mOm.js +2 -0
  22. package/dist/client/assets/{mapbox-gl-dgf_hX3a.js → mapbox-gl-WUhsQcSS.js} +1 -1
  23. package/dist/client/assets/{mentions-CxKCgzIf.js → mentions-C3JYXV2K.js} +1 -1
  24. package/dist/client/assets/network-map-D4XA7TTO.js +2 -0
  25. package/dist/client/assets/profile-analyze-QAznTMjZ.js +1 -0
  26. package/dist/client/assets/{profile-hydration-client-YEtJsNTv.js → profile-hydration-client-BQyC5--e.js} +1 -1
  27. package/dist/client/assets/profiles._handle-qntfbCE1.js +1 -0
  28. package/dist/client/assets/query-client-DZ5C7UNo.js +17 -0
  29. package/dist/client/assets/rate-limits-eHs_5dDB.js +1 -0
  30. package/dist/client/assets/react-BwGzW7mM.js +1 -0
  31. package/dist/client/assets/react-dom-DEMAB80t.js +1 -0
  32. package/dist/client/assets/{repeat-2-xCwHM9hD.js → repeat-2-CQ0lek01.js} +1 -1
  33. package/dist/client/assets/rolldown-runtime-B-9mzPXz.js +1 -0
  34. package/dist/client/assets/{routes-q7ZaW4En.js → routes-CeIv5bkV.js} +1 -1
  35. package/dist/client/assets/schemas-CntsNLmN.js +64 -0
  36. package/dist/client/assets/search-C_LqodNa.js +1 -0
  37. package/dist/client/assets/{sparkles-CQVlvHsZ.js → sparkles-Gx_BHkDX.js} +1 -1
  38. package/dist/client/assets/styles-DTMajAmk.css +2 -0
  39. package/dist/client/assets/today-DxppbQGF.js +1 -0
  40. package/dist/client/assets/{triangle-alert-CBL7s2pf.js → triangle-alert-DJPCW6jQ.js} +1 -1
  41. package/dist/client/assets/useTimelineRouteData-CWvBmHLE.js +1 -0
  42. package/dist/client/assets/{users-CHbKlqUr.js → users-DI3w8Mol.js} +1 -1
  43. package/dist/server/assets/{AvatarChip-zUDAEmjL.js → AvatarChip-DWag3IzT.js} +1 -1
  44. package/dist/server/assets/{FeedState-ntW0Q2GB.js → FeedState-CwzcvP-p.js} +1 -1
  45. package/dist/server/assets/{ProfileAnalysisStream-BFvhVdhn.js → ProfileAnalysisStream-CLjrYzEx.js} +2 -2
  46. package/dist/server/assets/{ProfilePreview-C6gAor_j.js → ProfilePreview-Bq-DSUM2.js} +7 -8
  47. package/dist/server/assets/{SavedTimelineView-BG1jnBp1.js → SavedTimelineView-Bk9nfnb-.js} +2 -2
  48. package/dist/server/assets/{SmartTimestamp-DfwE1NGf.js → SmartTimestamp-C06w_f9J.js} +1 -1
  49. package/dist/server/assets/SyncNowButton-CKuXKYcd.js +259 -0
  50. package/dist/server/assets/{TimelineRouteFrame-CU7ilM7U.js → TimelineRouteFrame-Bg3e7X_N.js} +5 -3
  51. package/dist/server/assets/{TweetRichText-DhCVv1-6.js → TweetRichText-wzUAtbPS.js} +2 -2
  52. package/dist/server/assets/{_tanstack-start-manifest_v-C-ZRnG_n.js → _tanstack-start-manifest_v-CjYYXQ8Z.js} +35 -35
  53. package/dist/server/assets/{blocks-DSgQghqP.js → blocks-CaP_u3mY.js} +3 -3
  54. package/dist/server/assets/{blocks-C1mgGTB3.js → blocks-DKy4yTF-.js} +3 -3
  55. package/dist/server/assets/{bookmarks-BJN8tykY.js → bookmarks-CPFq3JVX.js} +1 -1
  56. package/dist/server/assets/{client-stream-contracts-C2xbWVjm.js → client-stream-contracts-gtXEreFY.js} +72 -8
  57. package/dist/server/assets/{data-sources-BnA1Q94p.js → data-sources-DWwmQqcO.js} +1 -1
  58. package/dist/server/assets/{discuss-CJ1dau1j.js → discuss-BB8wFy4L.js} +3 -3
  59. package/dist/server/assets/{discuss-QTo08NCN.js → discuss-CVGB4_I0.js} +3 -3
  60. package/dist/server/assets/{dms-DAyQrVM2.js → dms-7NzdyQZQ.js} +6 -6
  61. package/dist/server/assets/{dms-BuLj4bp8.js → dms-vbEumuuO.js} +4 -4
  62. package/dist/server/assets/{empty-plugin-adapters-DzrEkdBF.js → empty-plugin-adapters-D9UWiqvJ.js} +1 -1
  63. package/dist/server/assets/{inbox-CkGOaW1q.js → inbox-CBeZctFd.js} +2 -2
  64. package/dist/server/assets/{inbox-DpWd1EzC.js → inbox-UpHhXODd.js} +4 -4
  65. package/dist/server/assets/{likes-DBOAtsJU.js → likes-CDAjYyTm.js} +1 -1
  66. package/dist/server/assets/{links-DAhSjynu.js → links-DU-TIHtg.js} +6 -6
  67. package/dist/server/assets/{links-DmH5lxHO.js → links-sQ3q2jvO.js} +6 -6
  68. package/dist/server/assets/{mentions-BH2MzKtB.js → mentions-DdndGSRw.js} +1 -1
  69. package/dist/server/assets/{network-map-D3CzRcC4.js → network-map-B9iOapuB.js} +2 -2
  70. package/dist/server/assets/{network-map-D5DoLOwR.js → network-map-CcOz-T9O.js} +2 -2
  71. package/dist/server/assets/{profile-analyze-C1NoM0tV.js → profile-analyze-BmDeTif5.js} +1 -1
  72. package/dist/server/assets/{profile-analyze-BP65PED0.js → profile-analyze-CUfXmXEH.js} +3 -3
  73. package/dist/server/assets/{profiles._handle-DsLNj2Ca.js → profiles._handle-8snagiR1.js} +4 -4
  74. package/dist/server/assets/{profiles._handle-DjwlN_ac.js → profiles._handle-BAqnghwo.js} +5 -5
  75. package/dist/server/assets/{rate-limits-VAr2qzYD.js → rate-limits-BY2-aQPT.js} +1 -1
  76. package/dist/server/assets/{router-DqkwSPiM.js → router-DP7iZD-q.js} +424 -128
  77. package/dist/server/assets/{routes-A9c9ZhwN.js → routes-D9226vKz.js} +1 -1
  78. package/dist/server/assets/start-5Z2QO8AU.js +4 -0
  79. package/dist/server/assets/{today-B5hVM57Z.js → today-BmvIMJzI.js} +3 -3
  80. package/dist/server/assets/{today-fvGiZ7ud.js → today-DKWphGrC.js} +112 -57
  81. package/dist/server/assets/{ui-qbmcMSSX.js → ui-rDHeS-cc.js} +2 -1
  82. package/dist/server/assets/{useTimelineRouteData-27cxynxa.js → useTimelineRouteData-BmDbGvQ8.js} +7 -7
  83. package/dist/server/server.js +50 -3413
  84. package/package.json +13 -13
  85. package/dist/client/assets/AvatarChip-DLoqM5xD.js +0 -1
  86. package/dist/client/assets/ProfilePreview-BejAuYsJ.js +0 -1
  87. package/dist/client/assets/SavedTimelineView-BIS7HKpp.js +0 -1
  88. package/dist/client/assets/SmartTimestamp-BR0-9se2.js +0 -1
  89. package/dist/client/assets/TimelineRouteFrame-CyGPj0eM.js +0 -1
  90. package/dist/client/assets/blocks-7UDKQp8H.js +0 -1
  91. package/dist/client/assets/chunk-C0phanGV.js +0 -1
  92. package/dist/client/assets/circle-check-C_Vq3ElD.js +0 -1
  93. package/dist/client/assets/client-stream-contracts-ys3XhWG4.js +0 -5
  94. package/dist/client/assets/createLucideIcon-JV8isFFR.js +0 -1
  95. package/dist/client/assets/data-sources-Cm2bpNvg.js +0 -1
  96. package/dist/client/assets/discuss-DTjaOugw.js +0 -1
  97. package/dist/client/assets/dms-CGd6Eogq.js +0 -1
  98. package/dist/client/assets/inbox-BHlGuOy2.js +0 -1
  99. package/dist/client/assets/index-CEIG7S6s.js +0 -31
  100. package/dist/client/assets/likes-cLrcAQem.js +0 -1
  101. package/dist/client/assets/links-CM9QO1LC.js +0 -1
  102. package/dist/client/assets/mapbox-BxTlqXt9.js +0 -2
  103. package/dist/client/assets/network-map-B5a3whqj.js +0 -2
  104. package/dist/client/assets/profile-analyze-CRtNEwlb.js +0 -1
  105. package/dist/client/assets/profiles._handle-C82_HoKX.js +0 -1
  106. package/dist/client/assets/query-client-fYu2rzRC.js +0 -17
  107. package/dist/client/assets/rate-limits-ScaEwdVK.js +0 -1
  108. package/dist/client/assets/react-dom-DHl6Ug7F.js +0 -1
  109. package/dist/client/assets/react-nBylq1U5.js +0 -1
  110. package/dist/client/assets/schemas-I9r6F6ya.js +0 -64
  111. package/dist/client/assets/search-Dq5kYsxA.js +0 -1
  112. package/dist/client/assets/styles-B8s3tt3r.css +0 -2
  113. package/dist/client/assets/today-CeG1O7Py.js +0 -1
  114. package/dist/client/assets/useTimelineRouteData-1HsqgvUi.js +0 -1
  115. package/dist/server/assets/SyncNowButton-BOn_IBXS.js +0 -76
  116. package/dist/server/assets/start-FaDOTmto.js +0 -4
@@ -1,19 +1,19 @@
1
- import { $t as sidebarBrandTaglineClass, Ct as navLinkLabelCompactClass, Qt as sidebarBrandMarkClass, St as navLinkLabelClass, Xt as sidebarBrandCopyClass, Yt as sidebarBrandClass, Zt as sidebarBrandCopyCompactClass, an as siteShellClass, bt as navLinkCompactClass, en as sidebarBrandTitleClass, gt as mainColumnDmClass, ht as mainColumnClass, in as sidebarShellCompactClass, m as cx, nn as sidebarNavClass, o as bodyClass, rn as sidebarShellClass, tn as sidebarFooterClass, xt as navLinkIconClass, yt as navLinkClass } from "./ui-qbmcMSSX.js";
1
+ import { $t as sidebarBrandMarkClass, Ct as navLinkLabelCompactClass, Qt as sidebarBrandCopyCompactClass, St as navLinkLabelClass, Xt as sidebarBrandClass, Zt as sidebarBrandCopyClass, an as sidebarShellCompactClass, bt as navLinkCompactClass, en as sidebarBrandTaglineClass, gt as mainColumnDmClass, ht as mainColumnClass, in as sidebarShellClass, m as cx, nn as sidebarFooterClass, o as bodyClass, on as siteShellClass, rn as sidebarNavClass, tn as sidebarBrandTitleClass, xt as navLinkIconClass, yt as navLinkClass } from "./ui-rDHeS-cc.js";
2
2
  import { C as networkMapResponseSchema, D as tweetConversationResponseSchema, E as queryResponseSchema, O as webSyncJobSchema, S as liveDataSourcesResponseSchema, T as queryEnvelopeSchema, _ as blockListResponseSchema, a as fetchQueryEnvelope, b as linkInsightResponseSchema, d as runEffectPromise, f as toError$17, g as actionResponseSchemaFor, h as actionRequestSchema, k as xurlRateLimitSnapshotSchema, l as defaultRuntimeServices, m as trySync$11, n as queryKeys, p as tryPromise, t as BirdclawQueryProvider, u as runEffectBackground, w as profileHydrationResponseSchema, x as linkPreviewResponseSchema, y as inboxResponseSchema } from "./query-client-BdauHBgd.js";
3
- import { t as AvatarChip } from "./AvatarChip-zUDAEmjL.js";
3
+ import { t as AvatarChip } from "./AvatarChip-DWag3IzT.js";
4
4
  import { n as setStoredAccountId, r as useSelectedAccountId } from "./account-selection-CTdFGWRk.js";
5
- import { c as BirdclawMark } from "./FeedState-ntW0Q2GB.js";
6
- import { a as assertSafePreviewUrl, h as tweetEntitiesFromXurl, i as __test__$1, l as displayUrlForLink, o as isBlockedAddress, s as safeHttpUrl, u as enrichFallbackUrlEntities } from "./ProfilePreview-C6gAor_j.js";
7
- import { i as searchDiscussionStreamEventSchema, n as periodDigestStreamEventSchema, r as profileAnalysisStreamEventSchema } from "./client-stream-contracts-C2xbWVjm.js";
8
- import { t as Route$25 } from "./today-B5hVM57Z.js";
9
- import { t as Route$26 } from "./profile-analyze-C1NoM0tV.js";
10
- import { t as Route$27 } from "./network-map-D3CzRcC4.js";
11
- import { t as Route$28 } from "./links-DAhSjynu.js";
12
- import { t as Route$29 } from "./inbox-DpWd1EzC.js";
13
- import { t as Route$30 } from "./dms-DAyQrVM2.js";
14
- import { t as Route$31 } from "./discuss-CJ1dau1j.js";
15
- import { t as Route$32 } from "./blocks-DSgQghqP.js";
16
- import { t as Route$33 } from "./profiles._handle-DsLNj2Ca.js";
5
+ import { c as BirdclawMark } from "./FeedState-CwzcvP-p.js";
6
+ import { a as assertSafePreviewUrl, h as tweetEntitiesFromXurl, i as __test__$1, l as displayUrlForLink, o as isBlockedAddress, s as safeHttpUrl, u as enrichFallbackUrlEntities } from "./ProfilePreview-Bq-DSUM2.js";
7
+ import { i as searchDiscussionStreamEventSchema, n as periodDigestStreamEventSchema, r as profileAnalysisStreamEventSchema } from "./client-stream-contracts-gtXEreFY.js";
8
+ import { t as Route$25 } from "./today-BmvIMJzI.js";
9
+ import { t as Route$26 } from "./profile-analyze-BmDeTif5.js";
10
+ import { t as Route$27 } from "./network-map-B9iOapuB.js";
11
+ import { t as Route$28 } from "./links-DU-TIHtg.js";
12
+ import { t as Route$29 } from "./inbox-UpHhXODd.js";
13
+ import { t as Route$30 } from "./dms-7NzdyQZQ.js";
14
+ import { t as Route$31 } from "./discuss-BB8wFy4L.js";
15
+ import { t as Route$32 } from "./blocks-CaP_u3mY.js";
16
+ import { t as Route$33 } from "./profiles._handle-8snagiR1.js";
17
17
  import { createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
18
18
  import { HeadContent, Link, Scripts, createFileRoute, createRootRoute, createRouter, lazyRouteComponent, useRouterState } from "@tanstack/react-router";
19
19
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -285,6 +285,7 @@ var cleanupThemeTransition = (root) => {
285
285
  root.classList.remove("theme-transition");
286
286
  root.style.removeProperty("--theme-switch-x");
287
287
  root.style.removeProperty("--theme-switch-y");
288
+ root.querySelectorAll("[data-theme-transition-target]").forEach((element) => element.removeAttribute("data-theme-transition-target"));
288
289
  };
289
290
  function startThemeTransition({ nextTheme, currentTheme, setTheme, context }) {
290
291
  if (currentTheme === nextTheme) return;
@@ -320,6 +321,7 @@ function startThemeTransition({ nextTheme, currentTheme, setTheme, context }) {
320
321
  root.style.setProperty("--theme-switch-x", `${xPercent * 100}%`);
321
322
  root.style.setProperty("--theme-switch-y", `${yPercent * 100}%`);
322
323
  root.classList.add("theme-transition");
324
+ context?.element?.setAttribute("data-theme-transition-target", "");
323
325
  try {
324
326
  const transition = startViewTransition?.(() => {
325
327
  applyTheme();
@@ -519,7 +521,7 @@ function AppNav({ compact = false }) {
519
521
  }
520
522
  //#endregion
521
523
  //#region src/styles.css?url
522
- var styles_default = "/assets/styles-B8s3tt3r.css";
524
+ var styles_default = "/assets/styles-DTMajAmk.css";
523
525
  //#endregion
524
526
  //#region src/routes/__root.tsx
525
527
  var Route$24 = createRootRoute({
@@ -572,31 +574,31 @@ function RootDocument({ children }) {
572
574
  }
573
575
  //#endregion
574
576
  //#region src/routes/rate-limits.tsx
575
- var $$splitComponentImporter$5 = () => import("./rate-limits-VAr2qzYD.js");
577
+ var $$splitComponentImporter$5 = () => import("./rate-limits-BY2-aQPT.js");
576
578
  var Route$23 = createFileRoute("/rate-limits")({ component: lazyRouteComponent($$splitComponentImporter$5, "component") });
577
579
  //#endregion
578
580
  //#region src/routes/mentions.tsx
579
- var $$splitComponentImporter$4 = () => import("./mentions-BH2MzKtB.js");
581
+ var $$splitComponentImporter$4 = () => import("./mentions-DdndGSRw.js");
580
582
  var Route$22 = createFileRoute("/mentions")({ component: lazyRouteComponent($$splitComponentImporter$4, "component") });
581
583
  //#endregion
582
584
  //#region src/routes/likes.tsx
583
- var $$splitComponentImporter$3 = () => import("./likes-DBOAtsJU.js");
585
+ var $$splitComponentImporter$3 = () => import("./likes-CDAjYyTm.js");
584
586
  var Route$21 = createFileRoute("/likes")({ component: lazyRouteComponent($$splitComponentImporter$3, "component") });
585
587
  //#endregion
586
588
  //#region src/routes/data-sources.tsx
587
- var $$splitComponentImporter$2 = () => import("./data-sources-BnA1Q94p.js");
589
+ var $$splitComponentImporter$2 = () => import("./data-sources-DWwmQqcO.js");
588
590
  var Route$20 = createFileRoute("/data-sources")({ component: lazyRouteComponent($$splitComponentImporter$2, "component") });
589
591
  //#endregion
590
592
  //#region src/routes/bookmarks.tsx
591
- var $$splitComponentImporter$1 = () => import("./bookmarks-BJN8tykY.js");
593
+ var $$splitComponentImporter$1 = () => import("./bookmarks-CPFq3JVX.js");
592
594
  var Route$19 = createFileRoute("/bookmarks")({ component: lazyRouteComponent($$splitComponentImporter$1, "component") });
593
595
  //#endregion
594
596
  //#region src/routes/index.tsx
595
- var $$splitComponentImporter = () => import("./routes-A9c9ZhwN.js");
597
+ var $$splitComponentImporter = () => import("./routes-D9226vKz.js");
596
598
  var Route$18 = createFileRoute("/")({ component: lazyRouteComponent($$splitComponentImporter, "component") });
597
599
  //#endregion
598
600
  //#region src/lib/http-effect.ts
599
- var LOCAL_HOSTS = new Set([
601
+ var LOCAL_HOSTS = /* @__PURE__ */ new Set([
600
602
  "localhost",
601
603
  "127.0.0.1",
602
604
  "::1",
@@ -1809,6 +1811,43 @@ var BASE_SCHEMA_SQL = `
1809
1811
  snapshot_id text not null
1810
1812
  );
1811
1813
 
1814
+ create table if not exists x_lists (
1815
+ account_id text not null,
1816
+ list_id text not null,
1817
+ name text not null,
1818
+ description text not null default '',
1819
+ owner_profile_id text,
1820
+ owner_external_user_id text,
1821
+ is_private integer not null default 0,
1822
+ member_count integer,
1823
+ follower_count integer,
1824
+ source text not null,
1825
+ membership_status text not null default 'not_synced',
1826
+ lists_synced_at text not null,
1827
+ members_synced_at text,
1828
+ member_page_count integer not null default 0,
1829
+ member_result_count integer not null default 0,
1830
+ rate_limit_json text not null default '{}',
1831
+ raw_json text not null default '{}',
1832
+ updated_at text not null,
1833
+ primary key (account_id, list_id)
1834
+ );
1835
+
1836
+ create table if not exists x_list_members (
1837
+ account_id text not null,
1838
+ list_id text not null,
1839
+ profile_id text not null,
1840
+ external_user_id text not null,
1841
+ source text not null,
1842
+ current integer not null default 1,
1843
+ first_seen_at text not null,
1844
+ last_seen_at text not null,
1845
+ ended_at text,
1846
+ raw_json text not null default '{}',
1847
+ updated_at text not null,
1848
+ primary key (account_id, list_id, profile_id)
1849
+ );
1850
+
1812
1851
  create table if not exists geocoded_locations (
1813
1852
  normalized_key text primary key,
1814
1853
  original text not null,
@@ -1881,6 +1920,9 @@ var INDEX_SQL = `
1881
1920
  create index if not exists idx_follow_edges_profile on follow_edges(profile_id, current);
1882
1921
  create index if not exists idx_follow_snapshots_account on follow_snapshots(account_id, direction, completed_at desc);
1883
1922
  create index if not exists idx_follow_events_account on follow_events(account_id, direction, kind, event_at desc);
1923
+ create index if not exists idx_x_lists_account_name on x_lists(account_id, name collate nocase, list_id);
1924
+ create index if not exists idx_x_list_members_current on x_list_members(account_id, list_id, current, profile_id);
1925
+ create index if not exists idx_x_list_members_profile on x_list_members(profile_id, current, account_id, list_id);
1884
1926
  create index if not exists idx_geocoded_locations_country on geocoded_locations(country_code);
1885
1927
  create index if not exists idx_geocoded_locations_last_used on geocoded_locations(last_used_at desc);
1886
1928
  create index if not exists idx_geocoded_unresolved_ttl on geocoded_locations_unresolved(ttl_until desc);
@@ -2096,6 +2138,50 @@ function ensureFollowGraphTables(db) {
2096
2138
  );
2097
2139
  `);
2098
2140
  }
2141
+ function ensureXListTables(db) {
2142
+ db.exec(`
2143
+ create table if not exists x_lists (
2144
+ account_id text not null,
2145
+ list_id text not null,
2146
+ name text not null,
2147
+ description text not null default '',
2148
+ owner_profile_id text,
2149
+ owner_external_user_id text,
2150
+ is_private integer not null default 0,
2151
+ member_count integer,
2152
+ follower_count integer,
2153
+ source text not null,
2154
+ membership_status text not null default 'not_synced',
2155
+ lists_synced_at text not null,
2156
+ members_synced_at text,
2157
+ member_page_count integer not null default 0,
2158
+ member_result_count integer not null default 0,
2159
+ rate_limit_json text not null default '{}',
2160
+ raw_json text not null default '{}',
2161
+ updated_at text not null,
2162
+ primary key (account_id, list_id)
2163
+ );
2164
+
2165
+ create table if not exists x_list_members (
2166
+ account_id text not null,
2167
+ list_id text not null,
2168
+ profile_id text not null,
2169
+ external_user_id text not null,
2170
+ source text not null,
2171
+ current integer not null default 1,
2172
+ first_seen_at text not null,
2173
+ last_seen_at text not null,
2174
+ ended_at text,
2175
+ raw_json text not null default '{}',
2176
+ updated_at text not null,
2177
+ primary key (account_id, list_id, profile_id)
2178
+ );
2179
+
2180
+ create index if not exists idx_x_lists_account_name on x_lists(account_id, name collate nocase, list_id);
2181
+ create index if not exists idx_x_list_members_current on x_list_members(account_id, list_id, current, profile_id);
2182
+ create index if not exists idx_x_list_members_profile on x_list_members(profile_id, current, account_id, list_id);
2183
+ `);
2184
+ }
2099
2185
  function backfillTweetCollections(db) {
2100
2186
  const missingKinds = [["likes", "liked"], ["bookmarks", "bookmarked"]].filter(([, column]) => db.prepare(`
2101
2187
  select 1
@@ -2198,32 +2284,42 @@ function normalizeTweetState(db) {
2198
2284
  create index idx_tweets_quoted on tweets(quoted_tweet_id);
2199
2285
  `);
2200
2286
  }
2201
- var DATABASE_MIGRATIONS = [{
2202
- version: 1,
2203
- name: "canonical schema baseline",
2204
- up: (db) => {
2205
- db.exec(BASE_SCHEMA_SQL);
2206
- ensureAccountExternalUserIdColumn(db);
2207
- ensureDmConversationInboxColumns(db);
2208
- ensureTweetMetadataColumns(db);
2209
- ensureProfileAvatarColumns(db);
2210
- ensureTweetCollectionsTable(db);
2211
- ensureTweetAccountEdgesTable(db);
2212
- ensureProfileAffiliationsTable(db);
2213
- ensureProfileSnapshotsTable(db);
2214
- ensureProfileBioEntitiesTable(db);
2215
- ensureIdentitySearchIndexTable(db);
2216
- ensureLinkIndexTables(db);
2217
- ensureFollowGraphTables(db);
2218
- ensureSchemaIndexes(db);
2219
- backfillTweetCollections(db);
2220
- backfillTweetAccountEdges(db);
2287
+ var DATABASE_MIGRATIONS = [
2288
+ {
2289
+ version: 1,
2290
+ name: "canonical schema baseline",
2291
+ up: (db) => {
2292
+ db.exec(BASE_SCHEMA_SQL);
2293
+ ensureAccountExternalUserIdColumn(db);
2294
+ ensureDmConversationInboxColumns(db);
2295
+ ensureTweetMetadataColumns(db);
2296
+ ensureProfileAvatarColumns(db);
2297
+ ensureTweetCollectionsTable(db);
2298
+ ensureTweetAccountEdgesTable(db);
2299
+ ensureProfileAffiliationsTable(db);
2300
+ ensureProfileSnapshotsTable(db);
2301
+ ensureProfileBioEntitiesTable(db);
2302
+ ensureIdentitySearchIndexTable(db);
2303
+ ensureLinkIndexTables(db);
2304
+ ensureFollowGraphTables(db);
2305
+ ensureSchemaIndexes(db);
2306
+ backfillTweetCollections(db);
2307
+ backfillTweetAccountEdges(db);
2308
+ }
2309
+ },
2310
+ {
2311
+ version: 2,
2312
+ name: "normalize tweet account and collection state",
2313
+ up: normalizeTweetState
2314
+ },
2315
+ {
2316
+ version: 3,
2317
+ name: "add durable X List metadata and membership",
2318
+ up: (db) => {
2319
+ ensureXListTables(db);
2320
+ }
2221
2321
  }
2222
- }, {
2223
- version: 2,
2224
- name: "normalize tweet account and collection state",
2225
- up: normalizeTweetState
2226
- }];
2322
+ ];
2227
2323
  function ensureDemoData(db) {
2228
2324
  if (demoSeedAttempted) return;
2229
2325
  seedDemoData(db);
@@ -2465,7 +2561,7 @@ function yearFromTimestamp(value) {
2465
2561
  function pathLeaf(relativePath) {
2466
2562
  return relativePath.split("/").at(-1) ?? "";
2467
2563
  }
2468
- var JSON_URL_KEYS = new Set([
2564
+ var JSON_URL_KEYS = /* @__PURE__ */ new Set([
2469
2565
  "url",
2470
2566
  "expandedUrl",
2471
2567
  "expanded_url",
@@ -2739,6 +2835,107 @@ var definitions = {
2739
2835
  ]
2740
2836
  }
2741
2837
  },
2838
+ x_lists: {
2839
+ exportSql: `
2840
+ select account_id, list_id, name, description, owner_profile_id,
2841
+ owner_external_user_id, is_private, member_count, follower_count,
2842
+ source, membership_status, lists_synced_at, members_synced_at,
2843
+ member_page_count, member_result_count, rate_limit_json, raw_json,
2844
+ updated_at
2845
+ from x_lists
2846
+ order by account_id, name collate nocase, list_id
2847
+ `,
2848
+ ...fixedShard("data/lists/lists.jsonl", "x_lists"),
2849
+ merge: {
2850
+ order: 9,
2851
+ sql: `
2852
+ insert into x_lists (
2853
+ account_id, list_id, name, description, owner_profile_id,
2854
+ owner_external_user_id, is_private, member_count, follower_count,
2855
+ source, membership_status, lists_synced_at, members_synced_at,
2856
+ member_page_count, member_result_count, rate_limit_json, raw_json,
2857
+ updated_at
2858
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2859
+ on conflict(account_id, list_id) do update set
2860
+ name = excluded.name,
2861
+ description = excluded.description,
2862
+ owner_profile_id = coalesce(excluded.owner_profile_id, x_lists.owner_profile_id),
2863
+ owner_external_user_id = coalesce(excluded.owner_external_user_id, x_lists.owner_external_user_id),
2864
+ is_private = case when excluded.updated_at >= x_lists.updated_at then excluded.is_private else x_lists.is_private end,
2865
+ member_count = coalesce(excluded.member_count, x_lists.member_count),
2866
+ follower_count = coalesce(excluded.follower_count, x_lists.follower_count),
2867
+ source = case when excluded.updated_at >= x_lists.updated_at then excluded.source else x_lists.source end,
2868
+ membership_status = case when excluded.updated_at >= x_lists.updated_at then excluded.membership_status else x_lists.membership_status end,
2869
+ lists_synced_at = max(x_lists.lists_synced_at, excluded.lists_synced_at),
2870
+ members_synced_at = nullif(max(coalesce(x_lists.members_synced_at, ''), coalesce(excluded.members_synced_at, '')), ''),
2871
+ member_page_count = case when excluded.updated_at >= x_lists.updated_at then excluded.member_page_count else x_lists.member_page_count end,
2872
+ member_result_count = case when excluded.updated_at >= x_lists.updated_at then excluded.member_result_count else x_lists.member_result_count end,
2873
+ rate_limit_json = case when excluded.updated_at >= x_lists.updated_at then excluded.rate_limit_json else x_lists.rate_limit_json end,
2874
+ raw_json = case when excluded.updated_at >= x_lists.updated_at then excluded.raw_json else x_lists.raw_json end,
2875
+ updated_at = max(x_lists.updated_at, excluded.updated_at)
2876
+ `,
2877
+ columns: [
2878
+ "account_id",
2879
+ "list_id",
2880
+ "name",
2881
+ "description",
2882
+ "owner_profile_id",
2883
+ "owner_external_user_id",
2884
+ "is_private",
2885
+ "member_count",
2886
+ "follower_count",
2887
+ "source",
2888
+ "membership_status",
2889
+ "lists_synced_at",
2890
+ "members_synced_at",
2891
+ "member_page_count",
2892
+ "member_result_count",
2893
+ "rate_limit_json",
2894
+ "raw_json",
2895
+ "updated_at"
2896
+ ]
2897
+ }
2898
+ },
2899
+ x_list_members: {
2900
+ exportSql: `
2901
+ select account_id, list_id, profile_id, external_user_id, source,
2902
+ current, first_seen_at, last_seen_at, ended_at, raw_json, updated_at
2903
+ from x_list_members
2904
+ order by account_id, list_id, profile_id
2905
+ `,
2906
+ ...fixedShard("data/lists/members.jsonl", "x_list_members"),
2907
+ merge: {
2908
+ order: 10,
2909
+ sql: `
2910
+ insert into x_list_members (
2911
+ account_id, list_id, profile_id, external_user_id, source, current,
2912
+ first_seen_at, last_seen_at, ended_at, raw_json, updated_at
2913
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2914
+ on conflict(account_id, list_id, profile_id) do update set
2915
+ external_user_id = coalesce(nullif(excluded.external_user_id, ''), x_list_members.external_user_id),
2916
+ source = case when excluded.updated_at >= x_list_members.updated_at then excluded.source else x_list_members.source end,
2917
+ current = case when excluded.updated_at >= x_list_members.updated_at then excluded.current else x_list_members.current end,
2918
+ first_seen_at = min(x_list_members.first_seen_at, excluded.first_seen_at),
2919
+ last_seen_at = max(x_list_members.last_seen_at, excluded.last_seen_at),
2920
+ ended_at = case when excluded.updated_at >= x_list_members.updated_at then excluded.ended_at else x_list_members.ended_at end,
2921
+ raw_json = case when excluded.updated_at >= x_list_members.updated_at then excluded.raw_json else x_list_members.raw_json end,
2922
+ updated_at = max(x_list_members.updated_at, excluded.updated_at)
2923
+ `,
2924
+ columns: [
2925
+ "account_id",
2926
+ "list_id",
2927
+ "profile_id",
2928
+ "external_user_id",
2929
+ "source",
2930
+ "current",
2931
+ "first_seen_at",
2932
+ "last_seen_at",
2933
+ "ended_at",
2934
+ "raw_json",
2935
+ "updated_at"
2936
+ ]
2937
+ }
2938
+ },
2742
2939
  tweets: {
2743
2940
  exportSql: `
2744
2941
  select id, author_profile_id, text, created_at, is_replied, reply_to_id,
@@ -2750,7 +2947,7 @@ var definitions = {
2750
2947
  matchesPath: (candidate) => candidate.startsWith("data/tweets/"),
2751
2948
  countKey: () => "tweets",
2752
2949
  merge: {
2753
- order: 9,
2950
+ order: 11,
2754
2951
  transform: sanitizeImportedTweets,
2755
2952
  sql: `
2756
2953
  insert into tweets (
@@ -2810,7 +3007,7 @@ var definitions = {
2810
3007
  matchesPath: (candidate) => candidate.startsWith("data/collections/"),
2811
3008
  countKey: (candidate) => `collections_${pathLeaf(candidate).replace(/\.jsonl$/, "") || "unknown"}`,
2812
3009
  merge: {
2813
- order: 10,
3010
+ order: 12,
2814
3011
  sql: `
2815
3012
  insert into tweet_collections (
2816
3013
  account_id, tweet_id, kind, collected_at, source, raw_json, updated_at
@@ -2848,7 +3045,7 @@ var definitions = {
2848
3045
  matchesPath: (candidate) => candidate.startsWith("data/timeline_edges/"),
2849
3046
  countKey: (candidate) => `timeline_edges_${pathLeaf(candidate).replace(/\.jsonl$/, "") || "unknown"}`,
2850
3047
  merge: {
2851
- order: 11,
3048
+ order: 13,
2852
3049
  sql: `
2853
3050
  insert into tweet_account_edges (
2854
3051
  account_id, tweet_id, kind, first_seen_at, last_seen_at, seen_count,
@@ -2887,7 +3084,7 @@ var definitions = {
2887
3084
  `,
2888
3085
  ...fixedShard("data/dms/conversations.jsonl", "dm_conversations"),
2889
3086
  merge: {
2890
- order: 12,
3087
+ order: 14,
2891
3088
  sql: `
2892
3089
  insert into dm_conversations (
2893
3090
  id, account_id, participant_profile_id, title, inbox_kind, last_message_at, unread_count, needs_reply
@@ -2928,7 +3125,7 @@ var definitions = {
2928
3125
  matchesPath: (candidate) => candidate.startsWith("data/dms/") && candidate !== "data/dms/conversations.jsonl",
2929
3126
  countKey: () => "dm_messages",
2930
3127
  merge: {
2931
- order: 13,
3128
+ order: 15,
2932
3129
  sql: `
2933
3130
  insert into dm_messages (
2934
3131
  id, conversation_id, sender_profile_id, text, created_at, direction, is_replied, media_count
@@ -2972,7 +3169,7 @@ var definitions = {
2972
3169
  `,
2973
3170
  ...fixedShard("data/links/url_expansions.jsonl", "url_expansions"),
2974
3171
  merge: {
2975
- order: 14,
3172
+ order: 16,
2976
3173
  transform: sanitizeImportedUrlExpansions,
2977
3174
  sql: `
2978
3175
  insert into url_expansions (
@@ -3020,7 +3217,7 @@ var definitions = {
3020
3217
  `,
3021
3218
  ...fixedShard("data/links/occurrences.jsonl", "link_occurrences"),
3022
3219
  merge: {
3023
- order: 15,
3220
+ order: 17,
3024
3221
  sql: `
3025
3222
  insert into link_occurrences (
3026
3223
  source_kind, source_id, source_position, short_url, account_id,
@@ -3052,7 +3249,7 @@ var definitions = {
3052
3249
  `,
3053
3250
  ...fixedShard("data/moderation/blocks.jsonl", "blocks"),
3054
3251
  merge: {
3055
- order: 16,
3252
+ order: 18,
3056
3253
  sql: `
3057
3254
  insert into blocks (account_id, profile_id, source, created_at)
3058
3255
  values (?, ?, ?, ?)
@@ -3076,7 +3273,7 @@ var definitions = {
3076
3273
  `,
3077
3274
  ...fixedShard("data/moderation/mutes.jsonl", "mutes"),
3078
3275
  merge: {
3079
- order: 17,
3276
+ order: 19,
3080
3277
  sql: `
3081
3278
  insert into mutes (account_id, profile_id, source, created_at)
3082
3279
  values (?, ?, ?, ?)
@@ -3100,7 +3297,7 @@ var definitions = {
3100
3297
  `,
3101
3298
  ...fixedShard("data/actions/tweet_actions.jsonl", "tweet_actions"),
3102
3299
  merge: {
3103
- order: 18,
3300
+ order: 20,
3104
3301
  sql: `
3105
3302
  insert into tweet_actions (id, account_id, tweet_id, kind, body, created_at)
3106
3303
  values (?, ?, ?, ?, ?, ?)
@@ -3129,7 +3326,7 @@ var definitions = {
3129
3326
  `,
3130
3327
  ...fixedShard("data/ai_scores.jsonl", "ai_scores"),
3131
3328
  merge: {
3132
- order: 19,
3329
+ order: 21,
3133
3330
  sql: `
3134
3331
  insert into ai_scores (
3135
3332
  entity_kind, entity_id, model, score, summary, reasoning, updated_at
@@ -3317,7 +3514,7 @@ function adaptLegacyTweetState(schemaVersion, tweets, collections, timelineEdges
3317
3514
  const nextTimelineEdges = [...timelineEdges];
3318
3515
  const collectionKeys = new Set(collections.map((row) => `${String(row.account_id)}\0${String(row.tweet_id)}\0${String(row.kind)}`));
3319
3516
  const edgeKeys = new Set(timelineEdges.map((row) => `${String(row.account_id)}\0${String(row.tweet_id)}\0${String(row.kind)}`));
3320
- const edgeKinds = new Set([
3517
+ const edgeKinds = /* @__PURE__ */ new Set([
3321
3518
  "home",
3322
3519
  "mention",
3323
3520
  "authored",
@@ -3491,6 +3688,8 @@ var ImportRepository = class {
3491
3688
  }
3492
3689
  clearBackupImport() {
3493
3690
  this.db.exec(`
3691
+ delete from x_list_members;
3692
+ delete from x_lists;
3494
3693
  delete from follow_events;
3495
3694
  delete from follow_edges;
3496
3695
  delete from follow_snapshot_members;
@@ -3797,10 +3996,11 @@ function runSubprocessEffect(options) {
3797
3996
  }
3798
3997
  //#endregion
3799
3998
  //#region src/lib/backup.ts
3800
- var BACKUP_SCHEMA_VERSION = 2;
3999
+ var BACKUP_SCHEMA_VERSION = 3;
3801
4000
  var MIN_SUPPORTED_BACKUP_SCHEMA_VERSION = 1;
3802
4001
  var MANIFEST_PATH = "manifest.json";
3803
4002
  var DATA_DIR = "data";
4003
+ var GITATTRIBUTES_PATH = ".gitattributes";
3804
4004
  var AUTO_SYNC_CACHE_KEY = "backup:auto-sync";
3805
4005
  var DEFAULT_STALE_AFTER_SECONDS = 900;
3806
4006
  var autoUpdateInFlight = null;
@@ -3959,6 +4159,8 @@ data/moderation/mutes.jsonl
3959
4159
  data/follow_snapshots.jsonl
3960
4160
  data/follow_snapshot_members.jsonl
3961
4161
  data/follow_edges.jsonl
4162
+ data/lists/lists.jsonl
4163
+ data/lists/members.jsonl
3962
4164
  data/follow_events.jsonl
3963
4165
  \`\`\`
3964
4166
 
@@ -3982,23 +4184,38 @@ function writeManifestEffect(repoPath, manifest) {
3982
4184
  function readPreviousManifestEffect(repoPath) {
3983
4185
  return readManifestEffect(repoPath).pipe(Effect.catchAll(() => Effect.succeed(void 0)));
3984
4186
  }
4187
+ function ensureBackupGitattributesEffect(repoPath) {
4188
+ return Effect.gen(function* () {
4189
+ const attributesPath = yield* trySync$10(() => resolveBackupFilePath(repoPath, GITATTRIBUTES_PATH));
4190
+ yield* assertNoSymlinkAncestorEffect(repoPath, attributesPath);
4191
+ const generatedBlock = [
4192
+ "# BEGIN birdclaw backup attributes",
4193
+ "# Backup hashes use the raw LF-delimited bytes written by Birdclaw.",
4194
+ ...["data/**/*.jsonl text eol=lf", `${MANIFEST_PATH} text eol=lf`],
4195
+ "# END birdclaw backup attributes",
4196
+ ""
4197
+ ].join("\n");
4198
+ const current = yield* tryPromise(() => fs.readFile(attributesPath, "utf8")).pipe(Effect.option);
4199
+ if (current._tag === "Some" && current.value.endsWith(generatedBlock)) return;
4200
+ const preserved = current._tag === "Some" ? current.value.replaceAll(generatedBlock, "").replace(/[\r\n]+$/u, "") : "";
4201
+ const content = preserved ? `${preserved}\n\n${generatedBlock}` : generatedBlock;
4202
+ yield* tryPromise(() => fs.writeFile(attributesPath, content, "utf8"));
4203
+ });
4204
+ }
3985
4205
  function maybeCommitAndPushEffect({ repoPath, message, commit, push }) {
3986
4206
  if (!commit && !push) return Effect.succeed(void 0);
3987
4207
  return Effect.gen(function* () {
3988
- yield* gitEffect([
3989
- "-C",
3990
- repoPath,
3991
- "rev-parse",
3992
- "--is-inside-work-tree"
3993
- ]).pipe(Effect.catchAll(() => gitEffect([
4208
+ if (!(yield* isGitRepoEffect(repoPath))) yield* gitEffect([
3994
4209
  "-C",
3995
4210
  repoPath,
3996
4211
  "init"
3997
- ]).pipe(Effect.asVoid)));
4212
+ ]);
4213
+ if (!(yield* isGitRepoEffect(repoPath))) return yield* Effect.fail(/* @__PURE__ */ new Error("Backup Git operations must run at the configured repository root"));
3998
4214
  yield* gitEffect([
3999
4215
  "-C",
4000
4216
  repoPath,
4001
4217
  "add",
4218
+ GITATTRIBUTES_PATH,
4002
4219
  "README.md",
4003
4220
  MANIFEST_PATH,
4004
4221
  DATA_DIR
@@ -4077,6 +4294,7 @@ function maybeCommitAndPushEffect({ repoPath, message, commit, push }) {
4077
4294
  });
4078
4295
  }
4079
4296
  function isGitRepoEffect(repoPath) {
4297
+ if (!existsSync(path.join(repoPath, ".git"))) return Effect.succeed(false);
4080
4298
  return gitEffect([
4081
4299
  "-C",
4082
4300
  repoPath,
@@ -4143,7 +4361,7 @@ function ensureBackupGitRepoEffect({ repoPath, remote }) {
4143
4361
  repoPath,
4144
4362
  "fetch",
4145
4363
  "origin",
4146
- "main"
4364
+ "main:refs/remotes/origin/main"
4147
4365
  ]).pipe(Effect.flatMap(() => gitEffect([
4148
4366
  "-C",
4149
4367
  repoPath,
@@ -4187,6 +4405,7 @@ function exportBackupEffect({ repoPath, db, commit = false, push = false, messag
4187
4405
  yield* tryPromise(() => fs.mkdir(resolvedRepoPath, { recursive: true }));
4188
4406
  const repoStat = yield* tryPromise(() => fs.lstat(resolvedRepoPath));
4189
4407
  if (!repoStat.isDirectory() || repoStat.isSymbolicLink()) return yield* Effect.fail(/* @__PURE__ */ new Error("Backup repository path must be a real directory"));
4408
+ yield* ensureBackupGitattributesEffect(resolvedRepoPath);
4190
4409
  yield* ensureBackupReadmeEffect(resolvedRepoPath);
4191
4410
  const shards = yield* trySync$10(() => buildShards(database));
4192
4411
  const shardEntries = yield* trySync$10(() => [...shards.entries()].sort(([left], [right]) => left.localeCompare(right)));
@@ -4851,6 +5070,12 @@ function runBirdJsonCommandEffect$1(args, timeoutMs) {
4851
5070
  });
4852
5071
  }));
4853
5072
  }
5073
+ function runBirdTweetJsonCommandEffect(args, timeoutMs) {
5074
+ return runBirdJsonCommandEffect$1([...args, "--json-full"], timeoutMs).pipe(Effect.catchAll((error) => {
5075
+ if (!isUnsupportedBirdOptionError(error, "--json-full")) return Effect.fail(error);
5076
+ return runBirdJsonCommandEffect$1([...args, "--json"], timeoutMs);
5077
+ }));
5078
+ }
4854
5079
  function getBirdTweetItems(payload, command) {
4855
5080
  if (Array.isArray(payload)) return payload;
4856
5081
  if (payload && typeof payload === "object" && Array.isArray(payload.tweets)) return payload.tweets;
@@ -4901,15 +5126,48 @@ function toReferencedTweets(item) {
4901
5126
  });
4902
5127
  return references.length > 0 ? references : void 0;
4903
5128
  }
5129
+ function getRecord(value) {
5130
+ return value && typeof value === "object" ? value : void 0;
5131
+ }
5132
+ function findGraphqlAvatarUrl(value, authorId) {
5133
+ const pending = [value];
5134
+ const seen = /* @__PURE__ */ new Set();
5135
+ while (pending.length > 0) {
5136
+ const record = getRecord(pending.pop());
5137
+ if (!record || seen.has(record)) continue;
5138
+ seen.add(record);
5139
+ if (record.rest_id === authorId) {
5140
+ const avatar = getRecord(record.avatar);
5141
+ if (typeof avatar?.image_url === "string" && avatar.image_url) return avatar.image_url;
5142
+ }
5143
+ for (const child of Object.values(record)) if (child && typeof child === "object") pending.push(child);
5144
+ }
5145
+ }
5146
+ function isHydratedBirdTweetItem(value) {
5147
+ const item = value;
5148
+ return typeof item?.id === "string" && typeof item.text === "string" && typeof item.createdAt === "string";
5149
+ }
4904
5150
  function normalizeBirdTweets(items) {
4905
5151
  const users = /* @__PURE__ */ new Map();
4906
- const data = items.map((item) => {
5152
+ const includedTweets = /* @__PURE__ */ new Map();
5153
+ const normalizeItem = (item, preserveMissingMetrics = false) => {
4907
5154
  const authorId = String(item.authorId ?? item.author?.username ?? "unknown");
4908
- if (!users.has(authorId)) users.set(authorId, {
5155
+ const profileImageUrl = item.author?.profileImageUrl ?? findGraphqlAvatarUrl(item._raw, authorId);
5156
+ const existingUser = users.get(authorId);
5157
+ if (!existingUser) users.set(authorId, {
4909
5158
  id: authorId,
4910
5159
  username: item.author?.username ?? `user_${authorId}`,
4911
- name: item.author?.name ?? item.author?.username ?? `user_${authorId}`
5160
+ name: item.author?.name ?? item.author?.username ?? `user_${authorId}`,
5161
+ ...profileImageUrl ? { profile_image_url: profileImageUrl } : {}
5162
+ });
5163
+ else if (profileImageUrl && !existingUser.profile_image_url) users.set(authorId, {
5164
+ ...existingUser,
5165
+ profile_image_url: profileImageUrl
4912
5166
  });
5167
+ if (isHydratedBirdTweetItem(item.quotedTweet)) {
5168
+ const quotedTweet = normalizeItem(item.quotedTweet, true);
5169
+ if (quotedTweet.id !== item.id) includedTweets.set(quotedTweet.id, quotedTweet);
5170
+ }
4913
5171
  return {
4914
5172
  id: item.id,
4915
5173
  author_id: authorId,
@@ -4918,17 +5176,25 @@ function normalizeBirdTweets(items) {
4918
5176
  conversation_id: item.conversationId ?? item.id,
4919
5177
  entities: toTweetEntities(item),
4920
5178
  referenced_tweets: toReferencedTweets(item),
4921
- public_metrics: {
5179
+ public_metrics: preserveMissingMetrics ? {
5180
+ ...item.replyCount === void 0 ? {} : { reply_count: Number(item.replyCount) },
5181
+ ...item.retweetCount === void 0 ? {} : { retweet_count: Number(item.retweetCount) },
5182
+ ...item.likeCount === void 0 ? {} : { like_count: Number(item.likeCount) }
5183
+ } : {
4922
5184
  reply_count: Number(item.replyCount ?? 0),
4923
5185
  retweet_count: Number(item.retweetCount ?? 0),
4924
5186
  like_count: Number(item.likeCount ?? 0)
4925
5187
  },
4926
5188
  edit_history_tweet_ids: [item.id]
4927
5189
  };
4928
- });
5190
+ };
5191
+ const data = items.map((item) => normalizeItem(item));
5192
+ const includes = {};
5193
+ if (users.size > 0) includes.users = Array.from(users.values());
5194
+ if (includedTweets.size > 0) includes.tweets = Array.from(includedTweets.values());
4929
5195
  return {
4930
5196
  data,
4931
- includes: users.size > 0 ? { users: Array.from(users.values()) } : void 0,
5197
+ includes: users.size > 0 || includedTweets.size > 0 ? includes : void 0,
4932
5198
  meta: {
4933
5199
  result_count: data.length,
4934
5200
  page_count: 1,
@@ -4952,11 +5218,10 @@ function normalizeBirdTweetsPayloadEffect(payload, command) {
4952
5218
  }
4953
5219
  function listMentionsViaBirdEffect({ maxResults }) {
4954
5220
  return Effect.gen(function* () {
4955
- return yield* normalizeBirdTweetsPayloadEffect(yield* parseBirdJsonEffect(yield* runBirdJsonCommandEffect$1([
5221
+ return yield* normalizeBirdTweetsPayloadEffect(yield* parseBirdJsonEffect(yield* runBirdTweetJsonCommandEffect([
4956
5222
  "mentions",
4957
5223
  "-n",
4958
- String(maxResults),
4959
- "--json"
5224
+ String(maxResults)
4960
5225
  ])), "mentions");
4961
5226
  });
4962
5227
  }
@@ -4965,12 +5230,13 @@ function listTweetsViaBirdCommandEffect({ command, maxResults, all, maxPages })
4965
5230
  const args = [
4966
5231
  command,
4967
5232
  "-n",
4968
- String(maxResults),
4969
- "--json"
5233
+ String(maxResults)
4970
5234
  ];
4971
- if (all) args.push("--all");
4972
- if (maxPages !== void 0) args.push("--max-pages", String(maxPages));
4973
- return yield* normalizeBirdTweetsPayloadEffect(yield* parseBirdJsonEffect(yield* runBirdJsonCommandEffect$1(args)), command);
5235
+ if (all) {
5236
+ args.push("--all");
5237
+ if (maxPages !== void 0) args.push("--max-pages", String(maxPages));
5238
+ }
5239
+ return yield* normalizeBirdTweetsPayloadEffect(yield* parseBirdJsonEffect(yield* runBirdTweetJsonCommandEffect(args)), command);
4974
5240
  });
4975
5241
  }
4976
5242
  function listLikedTweetsViaBirdEffect(options) {
@@ -4991,12 +5257,11 @@ function searchTweetsViaBirdEffect(query, options) {
4991
5257
  "search",
4992
5258
  query,
4993
5259
  "-n",
4994
- String(options.maxResults),
4995
- "--json"
5260
+ String(options.maxResults)
4996
5261
  ];
4997
5262
  if (options.all) args.push("--all");
4998
5263
  if (options.all && options.maxPages !== void 0) args.push("--max-pages", String(options.maxPages));
4999
- return yield* normalizeBirdTweetsPayloadEffect(yield* parseBirdJsonEffect(yield* runBirdJsonCommandEffect$1(args)), "search");
5264
+ return yield* normalizeBirdTweetsPayloadEffect(yield* parseBirdJsonEffect(yield* runBirdTweetJsonCommandEffect(args)), "search");
5000
5265
  });
5001
5266
  }
5002
5267
  function listHomeTimelineViaBirdEffect({ maxResults, following = true }) {
@@ -5004,11 +5269,10 @@ function listHomeTimelineViaBirdEffect({ maxResults, following = true }) {
5004
5269
  const args = [
5005
5270
  "home",
5006
5271
  "-n",
5007
- String(maxResults),
5008
- "--json"
5272
+ String(maxResults)
5009
5273
  ];
5010
5274
  if (following) args.push("--following");
5011
- return yield* normalizeBirdTweetsPayloadEffect(yield* parseBirdJsonEffect(yield* runBirdJsonCommandEffect$1(args)), "home");
5275
+ return yield* normalizeBirdTweetsPayloadEffect(yield* parseBirdJsonEffect(yield* runBirdTweetJsonCommandEffect(args)), "home");
5012
5276
  });
5013
5277
  }
5014
5278
  function normalizeBirdFollowUsers(payload, command, maxResults) {
@@ -5017,12 +5281,14 @@ function normalizeBirdFollowUsers(payload, command, maxResults) {
5017
5281
  if (!Array.isArray(users)) throw new Error(`bird ${command} returned unexpected JSON`);
5018
5282
  const data = users.map(toXurlMentionUser).filter((user) => Boolean(user));
5019
5283
  const nextToken = !Array.isArray(rawPayload) && typeof rawPayload.nextCursor === "string" ? rawPayload.nextCursor : null;
5284
+ const paginationKnownComplete = Array.isArray(rawPayload) ? data.length < maxResults : nextToken === null;
5020
5285
  return {
5021
5286
  data,
5022
5287
  meta: {
5023
5288
  result_count: data.length,
5024
5289
  page_count: data.length > 0 ? Math.max(1, Math.ceil(data.length / maxResults)) : 1,
5025
- next_token: nextToken
5290
+ next_token: nextToken,
5291
+ pagination_known_complete: paginationKnownComplete
5026
5292
  }
5027
5293
  };
5028
5294
  }
@@ -5048,14 +5314,10 @@ function listFollowUsersViaBirdEffect({ direction, userId, maxResults, all, maxP
5048
5314
  }
5049
5315
  function listThreadViaBirdEffect({ tweetId, all, maxPages, timeoutMs }) {
5050
5316
  return Effect.gen(function* () {
5051
- const args = [
5052
- "thread",
5053
- tweetId,
5054
- "--json"
5055
- ];
5317
+ const args = ["thread", tweetId];
5056
5318
  if (all) args.push("--all");
5057
5319
  if (maxPages !== void 0) args.push("--max-pages", String(maxPages));
5058
- return yield* normalizeBirdTweetsPayloadEffect(yield* parseBirdJsonEffect(yield* runBirdJsonCommandEffect$1(args, timeoutMs)), "thread");
5320
+ return yield* normalizeBirdTweetsPayloadEffect(yield* parseBirdJsonEffect(yield* runBirdTweetJsonCommandEffect(args, timeoutMs)), "thread");
5059
5321
  });
5060
5322
  }
5061
5323
  function normalizeBirdDmsPayloadEffect(payload) {
@@ -5514,7 +5776,7 @@ function runOAuth2JsonCommandEffect({ args, username, options, useConfiguredCand
5514
5776
  return yield* runJsonCommandEffect(oauth2ArgsForCandidate(authCandidate, args), scopedOptions).pipe(Effect.catchAll((error) => {
5515
5777
  if (!primaryUsername) return Effect.fail(error);
5516
5778
  oauth2CandidateCache.delete(primaryUsername.toLowerCase());
5517
- const attempted = new Set([authCandidate ? `${authCandidate.app ?? "default"}:${authCandidate.username}` : `default:${primaryUsername}`]);
5779
+ const attempted = /* @__PURE__ */ new Set([authCandidate ? `${authCandidate.app ?? "default"}:${authCandidate.username}` : `default:${primaryUsername}`]);
5518
5780
  if (authCandidate?.username !== primaryUsername) attempted.add(`default:${primaryUsername}`);
5519
5781
  return lookupOAuth2UsernameForAccountEffect(primaryUsername, attempted, deadlineMs).pipe(Effect.flatMap((fallbackUsername) => {
5520
5782
  if (!fallbackUsername) return Effect.fail(error);
@@ -6150,8 +6412,8 @@ var MAX_AVATAR_BYTES = 2 * 1024 * 1024;
6150
6412
  var MAX_AVATAR_DATA_URL_CHARS = MAX_AVATAR_BYTES * 4;
6151
6413
  var REMOTE_AVATAR_TIMEOUT_MS = 1e4;
6152
6414
  var DEFAULT_AVATAR_PREFETCH_CONCURRENCY = 8;
6153
- var ALLOWED_REMOTE_AVATAR_HOSTS = new Set(["pbs.twimg.com"]);
6154
- var RASTER_CONTENT_TYPES = new Set([
6415
+ var ALLOWED_REMOTE_AVATAR_HOSTS = /* @__PURE__ */ new Set(["pbs.twimg.com"]);
6416
+ var RASTER_CONTENT_TYPES = /* @__PURE__ */ new Set([
6155
6417
  "image/jpeg",
6156
6418
  "image/png",
6157
6419
  "image/webp",
@@ -7747,6 +8009,12 @@ function buildMediaJsonFromIncludes(tweet, media = []) {
7747
8009
  function getReferencedTweetId(tweet, type) {
7748
8010
  return tweet.referenced_tweets?.find((item) => item.type === type)?.id ?? null;
7749
8011
  }
8012
+ function toCanonicalTweets(payload) {
8013
+ const tweetsById = /* @__PURE__ */ new Map();
8014
+ for (const tweet of payload.includes?.tweets ?? []) tweetsById.set(tweet.id, tweet);
8015
+ for (const tweet of payload.data) tweetsById.set(tweet.id, tweet);
8016
+ return tweetsById.values();
8017
+ }
7750
8018
  function replaceTweetFts$1(db, tweetId, text) {
7751
8019
  db.prepare("delete from tweets_fts where tweet_id = ?").run(tweetId);
7752
8020
  db.prepare("insert into tweets_fts (tweet_id, text) values (?, ?)").run(tweetId, text);
@@ -7764,7 +8032,7 @@ function ingestTweetPayload(db, { accountId, payload, source, edgeKind, collecti
7764
8032
  created_at = excluded.created_at,
7765
8033
  is_replied = max(tweets.is_replied, excluded.is_replied),
7766
8034
  reply_to_id = coalesce(tweets.reply_to_id, excluded.reply_to_id),
7767
- like_count = excluded.like_count,
8035
+ like_count = case when ? then tweets.like_count else excluded.like_count end,
7768
8036
  media_count = max(tweets.media_count, excluded.media_count),
7769
8037
  entities_json = excluded.entities_json,
7770
8038
  media_json = case
@@ -7785,13 +8053,16 @@ function ingestTweetPayload(db, { accountId, payload, source, edgeKind, collecti
7785
8053
  const tweetIds = [];
7786
8054
  db.transaction(() => {
7787
8055
  const observedAt = (/* @__PURE__ */ new Date()).toISOString();
7788
- for (const tweet of payload.data) {
8056
+ const primaryTweetIds = new Set(payload.data.map((tweet) => tweet.id));
8057
+ for (const tweet of toCanonicalTweets(payload)) {
8058
+ const isPrimaryTweet = primaryTweetIds.has(tweet.id);
7789
8059
  const author = usersById.get(tweet.author_id);
7790
8060
  const profile = author ? upsertProfileFromXUser(db, author) : ensureStubProfileForXUser(db, tweet.author_id);
7791
8061
  const replyToId = getReferencedTweetId(tweet, "replied_to");
7792
8062
  const quotedTweetId = getReferencedTweetId(tweet, "quoted");
7793
- upsertTweet.run(tweet.id, profile.profile.id, tweet.text, tweet.created_at, markRepliesAsReplied && replyToId ? 1 : 0, replyToId, Number(tweet.public_metrics?.like_count ?? 0), countTweetMedia(tweet), JSON.stringify(tweetEntitiesFromXurl(tweet.entities)), buildMediaJsonFromIncludes(tweet, payload.includes?.media), quotedTweetId);
7794
- if (edgeKind) upsertTweetAccountEdge(db, {
8063
+ const shouldMarkReplied = isPrimaryTweet && markRepliesAsReplied && Boolean(replyToId);
8064
+ upsertTweet.run(tweet.id, profile.profile.id, tweet.text, tweet.created_at, shouldMarkReplied ? 1 : 0, replyToId, Number(tweet.public_metrics?.like_count ?? 0), countTweetMedia(tweet), JSON.stringify(tweetEntitiesFromXurl(tweet.entities)), buildMediaJsonFromIncludes(tweet, payload.includes?.media), quotedTweetId, !isPrimaryTweet && tweet.public_metrics?.like_count === void 0 ? 1 : 0);
8065
+ if (edgeKind && isPrimaryTweet) upsertTweetAccountEdge(db, {
7795
8066
  accountId,
7796
8067
  tweetId: tweet.id,
7797
8068
  kind: edgeKind,
@@ -7799,9 +8070,9 @@ function ingestTweetPayload(db, { accountId, payload, source, edgeKind, collecti
7799
8070
  seenAt: observedAt,
7800
8071
  rawJson: JSON.stringify(tweet)
7801
8072
  });
7802
- upsertCollection?.run(accountId, tweet.id, collectionKind, source, JSON.stringify(tweet), observedAt);
8073
+ if (isPrimaryTweet) upsertCollection?.run(accountId, tweet.id, collectionKind, source, JSON.stringify(tweet), observedAt);
7803
8074
  replaceTweetFts$1(db, tweet.id, tweet.text);
7804
- tweetIds.push(tweet.id);
8075
+ if (isPrimaryTweet) tweetIds.push(tweet.id);
7805
8076
  }
7806
8077
  })();
7807
8078
  return tweetIds;
@@ -7940,7 +8211,7 @@ function listMentionsByIds(db, accountId, tweetIds, limit) {
7940
8211
  }
7941
8212
  function mergeMentionThreadIntoLocalStore({ db, accountId, accountHandle, mentionIds, payload, source = "bird", writeThreadContextEdges = false }) {
7942
8213
  const usersById = new Map((payload.includes?.users ?? []).map((user) => [user.id, user]));
7943
- const groups = new Map([
8214
+ const groups = /* @__PURE__ */ new Map([
7944
8215
  ["mention", []],
7945
8216
  ["home", []],
7946
8217
  ["thread", []]
@@ -8001,7 +8272,7 @@ function fetchParentChainViaXurlEffect({ mention, maxDepth, timeoutMs, deadlineM
8001
8272
  return Effect.gen(function* () {
8002
8273
  const pages = [];
8003
8274
  const warnings = [];
8004
- const seenTweetIds = new Set([mention.id]);
8275
+ const seenTweetIds = /* @__PURE__ */ new Set([mention.id]);
8005
8276
  let nextParentId = mention.replyToId;
8006
8277
  let fallbackDepth = 0;
8007
8278
  let generalReadTweets = 0;
@@ -8047,7 +8318,7 @@ function fetchParentChainViaXurlEffect({ mention, maxDepth, timeoutMs, deadlineM
8047
8318
  }
8048
8319
  function findMissingAncestorId(mention, payload) {
8049
8320
  const tweetsById = new Map(payload.data.map((tweet) => [tweet.id, tweet]));
8050
- const seenTweetIds = new Set([mention.id]);
8321
+ const seenTweetIds = /* @__PURE__ */ new Set([mention.id]);
8051
8322
  const anchorTweet = tweetsById.get(mention.id) ?? mention.rawTweet;
8052
8323
  let nextParentId = anchorTweet ? getReplyToId(anchorTweet) : mention.replyToId;
8053
8324
  while (nextParentId) {
@@ -8539,7 +8810,7 @@ function getTimelineQualityReason(row, lowQualityThreshold) {
8539
8810
  return "keep:long-text";
8540
8811
  }
8541
8812
  var RECENT_TIMELINE_EDGE_CANDIDATES = 5e3;
8542
- function listTimelineItems({ resource, account, search, replyFilter = "all", since, until, untilId, includeReplies = true, qualityFilter = "all", lowQualityThreshold, includeQualityReason = false, likedOnly = false, bookmarkedOnly = false, limit = 18 }) {
8813
+ function listTimelineItems({ resource, account, listAccountId, listId, search, replyFilter = "all", since, until, untilId, includeReplies = true, qualityFilter = "all", lowQualityThreshold, includeQualityReason = false, likedOnly = false, bookmarkedOnly = false, limit = 18 }) {
8543
8814
  const db = getReadDb();
8544
8815
  const kind = resource === "mentions" ? "mention" : resource;
8545
8816
  const params = [];
@@ -8557,13 +8828,13 @@ function listTimelineItems({ resource, account, search, replyFilter = "all", sin
8557
8828
  let join = "";
8558
8829
  let where = "where e.kind = ?";
8559
8830
  let searchSnippetSelect = "";
8560
- const canUseRecentEdgeWindow = !likedOnly && !bookmarkedOnly && !account && !search?.trim() && replyFilter === "all" && !since?.trim() && !until?.trim() && includeReplies && qualityFilter === "all";
8831
+ const canUseRecentEdgeWindow = !likedOnly && !bookmarkedOnly && !listId && !account && !search?.trim() && replyFilter === "all" && !since?.trim() && !until?.trim() && includeReplies && qualityFilter === "all";
8561
8832
  if (likedOnly || bookmarkedOnly) {
8562
8833
  if (likedOnly && bookmarkedOnly) timelineEdgesCte = `
8563
8834
  with timeline_edges as (
8564
8835
  select likes.account_id, likes.tweet_id, 'home' as kind, likes.raw_json
8565
- from tweet_collections likes
8566
- join tweet_collections bookmarks
8836
+ from tweet_collections likes indexed by idx_tweet_collections_tweet
8837
+ join tweet_collections bookmarks indexed by idx_tweet_collections_tweet
8567
8838
  on bookmarks.account_id = likes.account_id
8568
8839
  and bookmarks.tweet_id = likes.tweet_id
8569
8840
  and bookmarks.kind = 'bookmarks'
@@ -8575,7 +8846,7 @@ function listTimelineItems({ resource, account, search, replyFilter = "all", sin
8575
8846
  timelineEdgesCte = `
8576
8847
  with timeline_edges as (
8577
8848
  select account_id, tweet_id, 'home' as kind, raw_json
8578
- from tweet_collections
8849
+ from tweet_collections indexed by idx_tweet_collections_tweet
8579
8850
  where kind = ?
8580
8851
  )
8581
8852
  `;
@@ -8637,6 +8908,20 @@ function listTimelineItems({ resource, account, search, replyFilter = "all", sin
8637
8908
  where += " and t.created_at < ?";
8638
8909
  params.push(until.trim());
8639
8910
  }
8911
+ if (Boolean(listId) !== Boolean(listAccountId)) throw new Error("List filtering requires both listId and listAccountId");
8912
+ if (listId && listAccountId) {
8913
+ where += `
8914
+ and exists (
8915
+ select 1
8916
+ from x_list_members list_member
8917
+ where list_member.account_id = ?
8918
+ and list_member.list_id = ?
8919
+ and list_member.profile_id = t.author_profile_id
8920
+ and list_member.current = 1
8921
+ )
8922
+ `;
8923
+ params.push(listAccountId, listId);
8924
+ }
8640
8925
  const ftsSearch = search?.trim() ? toFtsSearchQuery(search) : "";
8641
8926
  if (ftsSearch) {
8642
8927
  join += " join tweets_fts on tweets_fts.tweet_id = t.id ";
@@ -9654,6 +9939,7 @@ function reachedStartTimeBoundary(payload, startTimeMs) {
9654
9939
  function mergeTimelinePayloads(payloads, limit) {
9655
9940
  const data = [];
9656
9941
  const usersById = /* @__PURE__ */ new Map();
9942
+ const tweetsById = /* @__PURE__ */ new Map();
9657
9943
  const mediaByKey = /* @__PURE__ */ new Map();
9658
9944
  let meta;
9659
9945
  for (const payload of payloads) {
@@ -9664,6 +9950,7 @@ function mergeTimelinePayloads(payloads, limit) {
9664
9950
  if (data.length >= limit) break;
9665
9951
  }
9666
9952
  for (const user of payload.includes?.users ?? []) usersById.set(user.id, user);
9953
+ for (const tweet of payload.includes?.tweets ?? []) tweetsById.set(tweet.id, tweet);
9667
9954
  for (const media of payload.includes?.media ?? []) mediaByKey.set(media.media_key, media);
9668
9955
  if (data.length >= limit) break;
9669
9956
  }
@@ -9671,6 +9958,7 @@ function mergeTimelinePayloads(payloads, limit) {
9671
9958
  data,
9672
9959
  includes: {
9673
9960
  users: [...usersById.values()],
9961
+ tweets: [...tweetsById.values()],
9674
9962
  media: [...mediaByKey.values()]
9675
9963
  },
9676
9964
  meta
@@ -11119,7 +11407,7 @@ function runModeEffect(mode, options) {
11119
11407
  }
11120
11408
  function combineTweetSearchResults(left, right, limit) {
11121
11409
  if (left.ok && right.ok) {
11122
- const tweetIds = [...new Set([...left.tweetIds, ...right.tweetIds])].slice(0, limit);
11410
+ const tweetIds = [.../* @__PURE__ */ new Set([...left.tweetIds, ...right.tweetIds])].slice(0, limit);
11123
11411
  const liveSources = new Set([left.source, right.source].filter((source) => source !== "cache"));
11124
11412
  return {
11125
11413
  ok: true,
@@ -11257,6 +11545,8 @@ function compactTweet$1(source, item) {
11257
11545
  authorProfile: item.author,
11258
11546
  createdAt: item.createdAt,
11259
11547
  text: item.text,
11548
+ entities: item.entities,
11549
+ media: item.media,
11260
11550
  likeCount: item.likeCount,
11261
11551
  liked: item.liked,
11262
11552
  bookmarked: item.bookmarked,
@@ -11291,7 +11581,9 @@ function collectLiveSearchTweets(options) {
11291
11581
  t.created_at,
11292
11582
  t.is_replied,
11293
11583
  t.like_count,
11294
- t.media_count,
11584
+ t.media_count,
11585
+ t.entities_json,
11586
+ t.media_json,
11295
11587
  case
11296
11588
  when exists (
11297
11589
  select 1 from tweet_collections collection
@@ -11341,6 +11633,8 @@ function collectLiveSearchTweets(options) {
11341
11633
  },
11342
11634
  createdAt: String(row.created_at),
11343
11635
  text: String(row.text),
11636
+ entities: parseJsonField$1(row.entities_json, {}),
11637
+ media: parseJsonField$1(row.media_json, []),
11344
11638
  likeCount: Number(row.like_count),
11345
11639
  liked: Boolean(row.liked),
11346
11640
  bookmarked: Boolean(row.bookmarked),
@@ -12720,7 +13014,7 @@ var Route$11 = createFileRoute("/api/profile-analysis")({ server: { handlers: {
12720
13014
  //#region src/lib/link-insights.ts
12721
13015
  var DEFAULT_LIMIT$1 = 30;
12722
13016
  var DEFAULT_COMMENTS_LIMIT = 8;
12723
- var TRACKING_PARAMS = new Set([
13017
+ var TRACKING_PARAMS = /* @__PURE__ */ new Set([
12724
13018
  "fbclid",
12725
13019
  "gclid",
12726
13020
  "igshid",
@@ -12739,17 +13033,17 @@ var VIDEO_HOST_SUFFIXES = [
12739
13033
  "tiktok.com",
12740
13034
  "loom.com"
12741
13035
  ];
12742
- var VIDEO_EXACT_HOSTS = new Set([
13036
+ var VIDEO_EXACT_HOSTS = /* @__PURE__ */ new Set([
12743
13037
  "youtu.be",
12744
13038
  "clips.twitch.tv",
12745
13039
  "vm.tiktok.com"
12746
13040
  ]);
12747
13041
  var EXCLUDED_HOST_SUFFIXES = ["x.com", "twitter.com"];
12748
- var EXCLUDED_EXACT_HOSTS = new Set(["t.co"]);
13042
+ var EXCLUDED_EXACT_HOSTS = /* @__PURE__ */ new Set(["t.co"]);
12749
13043
  var URL_WHITESPACE = /\s+/g;
12750
13044
  var RAW_URL_PATTERN = /https?:\/\/[^\s<>"'`]+/g;
12751
13045
  var SQL_URL_EXPRESSION = "lower(coalesce(nullif(e.final_url, ''), nullif(e.expanded_url, ''), e.short_url))";
12752
- var TITLE_SEGMENT_STOPWORDS = new Set([
13046
+ var TITLE_SEGMENT_STOPWORDS = /* @__PURE__ */ new Set([
12753
13047
  "a",
12754
13048
  "about",
12755
13049
  "blog",
@@ -12762,7 +13056,7 @@ var TITLE_SEGMENT_STOPWORDS = new Set([
12762
13056
  "story",
12763
13057
  "watch"
12764
13058
  ]);
12765
- var TITLE_WORD_OVERRIDES = new Map([
13059
+ var TITLE_WORD_OVERRIDES = /* @__PURE__ */ new Map([
12766
13060
  ["ai", "AI"],
12767
13061
  ["api", "API"],
12768
13062
  ["codex", "Codex"],
@@ -12773,7 +13067,7 @@ var TITLE_WORD_OVERRIDES = new Map([
12773
13067
  ["url", "URL"],
12774
13068
  ["wwdc", "WWDC"]
12775
13069
  ]);
12776
- var TITLE_SMALL_WORDS = new Set([
13070
+ var TITLE_SMALL_WORDS = /* @__PURE__ */ new Set([
12777
13071
  "a",
12778
13072
  "an",
12779
13073
  "and",
@@ -12922,7 +13216,7 @@ function stripUrls(text, urls) {
12922
13216
  let output = text;
12923
13217
  for (const url of urls) {
12924
13218
  if (!url) continue;
12925
- const variants = new Set([url]);
13219
+ const variants = /* @__PURE__ */ new Set([url]);
12926
13220
  try {
12927
13221
  const parsed = new URL(url);
12928
13222
  variants.add(parsed.href);
@@ -13309,9 +13603,9 @@ function getLinkInsights(query = {}) {
13309
13603
  kind: rowKind,
13310
13604
  lastSeenAt: row.created_at,
13311
13605
  mentions: [mention],
13312
- seenMentions: new Set([mention.id]),
13313
- sharers: new Set([sharerKey]),
13314
- sharerProfiles: mention.sharedBy ? new Map([[mention.sharedBy.id, mention.sharedBy]]) : /* @__PURE__ */ new Map(),
13606
+ seenMentions: /* @__PURE__ */ new Set([mention.id]),
13607
+ sharers: /* @__PURE__ */ new Set([sharerKey]),
13608
+ sharerProfiles: mention.sharedBy ? /* @__PURE__ */ new Map([[mention.sharedBy.id, mention.sharedBy]]) : /* @__PURE__ */ new Map(),
13315
13609
  shareCount: 1,
13316
13610
  title: row.title,
13317
13611
  topSharer: mention.sharedBy ?? null,
@@ -13487,6 +13781,7 @@ function compactTweet(source, item) {
13487
13781
  createdAt: item.createdAt,
13488
13782
  text: item.text,
13489
13783
  entities: item.entities,
13784
+ media: item.media,
13490
13785
  likeCount: item.likeCount,
13491
13786
  liked: item.liked,
13492
13787
  bookmarked: item.bookmarked,
@@ -13506,6 +13801,7 @@ function compactEmbeddedTweet(item) {
13506
13801
  createdAt: item.createdAt,
13507
13802
  text: item.text,
13508
13803
  entities: item.entities,
13804
+ media: item.media,
13509
13805
  likeCount: item.likeCount ?? 0,
13510
13806
  liked: Boolean(item.liked),
13511
13807
  bookmarked: Boolean(item.bookmarked),
@@ -14175,7 +14471,7 @@ var LETTER_PATTERN = /[a-z]/i;
14175
14471
  var URL_OR_HANDLE_PATTERN = /(https?:\/\/|@|[A-Z0-9._%+-]+@[A-Z0-9.-]+)/i;
14176
14472
  var MONEY_PATTERN = /\$\s?\d/;
14177
14473
  var EMOJI_BLOCK_PATTERN = /[\u{1f1e6}-\u{1f1ff}\u{1f300}-\u{1faff}\u{2600}-\u{27bf}]/gu;
14178
- var GENERIC_LOCATIONS = new Set([
14474
+ var GENERIC_LOCATIONS = /* @__PURE__ */ new Set([
14179
14475
  "earth",
14180
14476
  "everywhere",
14181
14477
  "internet",
@@ -14193,7 +14489,7 @@ var GENERIC_LOCATIONS = new Set([
14193
14489
  "world",
14194
14490
  "world wide web"
14195
14491
  ]);
14196
- var DROP_WORDS = new Set([
14492
+ var DROP_WORDS = /* @__PURE__ */ new Set([
14197
14493
  "a",
14198
14494
  "an",
14199
14495
  "and",
@@ -14747,7 +15043,7 @@ function upsertUrlExpansion(db, item) {
14747
15043
  var FETCH_TIMEOUT_MS = 12e3;
14748
15044
  var MAX_HTML_CHARS = 2e6;
14749
15045
  var MAX_REDIRECTS = 4;
14750
- var NO_BODY_STATUS_CODES = new Set([
15046
+ var NO_BODY_STATUS_CODES = /* @__PURE__ */ new Set([
14751
15047
  204,
14752
15048
  205,
14753
15049
  304