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
@@ -487,6 +487,107 @@ var definitions = {
487
487
  ]
488
488
  }
489
489
  },
490
+ x_lists: {
491
+ exportSql: `
492
+ select account_id, list_id, name, description, owner_profile_id,
493
+ owner_external_user_id, is_private, member_count, follower_count,
494
+ source, membership_status, lists_synced_at, members_synced_at,
495
+ member_page_count, member_result_count, rate_limit_json, raw_json,
496
+ updated_at
497
+ from x_lists
498
+ order by account_id, name collate nocase, list_id
499
+ `,
500
+ ...fixedShard("data/lists/lists.jsonl", "x_lists"),
501
+ merge: {
502
+ order: 9,
503
+ sql: `
504
+ insert into x_lists (
505
+ account_id, list_id, name, description, owner_profile_id,
506
+ owner_external_user_id, is_private, member_count, follower_count,
507
+ source, membership_status, lists_synced_at, members_synced_at,
508
+ member_page_count, member_result_count, rate_limit_json, raw_json,
509
+ updated_at
510
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
511
+ on conflict(account_id, list_id) do update set
512
+ name = excluded.name,
513
+ description = excluded.description,
514
+ owner_profile_id = coalesce(excluded.owner_profile_id, x_lists.owner_profile_id),
515
+ owner_external_user_id = coalesce(excluded.owner_external_user_id, x_lists.owner_external_user_id),
516
+ is_private = case when excluded.updated_at >= x_lists.updated_at then excluded.is_private else x_lists.is_private end,
517
+ member_count = coalesce(excluded.member_count, x_lists.member_count),
518
+ follower_count = coalesce(excluded.follower_count, x_lists.follower_count),
519
+ source = case when excluded.updated_at >= x_lists.updated_at then excluded.source else x_lists.source end,
520
+ membership_status = case when excluded.updated_at >= x_lists.updated_at then excluded.membership_status else x_lists.membership_status end,
521
+ lists_synced_at = max(x_lists.lists_synced_at, excluded.lists_synced_at),
522
+ members_synced_at = nullif(max(coalesce(x_lists.members_synced_at, ''), coalesce(excluded.members_synced_at, '')), ''),
523
+ member_page_count = case when excluded.updated_at >= x_lists.updated_at then excluded.member_page_count else x_lists.member_page_count end,
524
+ member_result_count = case when excluded.updated_at >= x_lists.updated_at then excluded.member_result_count else x_lists.member_result_count end,
525
+ rate_limit_json = case when excluded.updated_at >= x_lists.updated_at then excluded.rate_limit_json else x_lists.rate_limit_json end,
526
+ raw_json = case when excluded.updated_at >= x_lists.updated_at then excluded.raw_json else x_lists.raw_json end,
527
+ updated_at = max(x_lists.updated_at, excluded.updated_at)
528
+ `,
529
+ columns: [
530
+ "account_id",
531
+ "list_id",
532
+ "name",
533
+ "description",
534
+ "owner_profile_id",
535
+ "owner_external_user_id",
536
+ "is_private",
537
+ "member_count",
538
+ "follower_count",
539
+ "source",
540
+ "membership_status",
541
+ "lists_synced_at",
542
+ "members_synced_at",
543
+ "member_page_count",
544
+ "member_result_count",
545
+ "rate_limit_json",
546
+ "raw_json",
547
+ "updated_at"
548
+ ]
549
+ }
550
+ },
551
+ x_list_members: {
552
+ exportSql: `
553
+ select account_id, list_id, profile_id, external_user_id, source,
554
+ current, first_seen_at, last_seen_at, ended_at, raw_json, updated_at
555
+ from x_list_members
556
+ order by account_id, list_id, profile_id
557
+ `,
558
+ ...fixedShard("data/lists/members.jsonl", "x_list_members"),
559
+ merge: {
560
+ order: 10,
561
+ sql: `
562
+ insert into x_list_members (
563
+ account_id, list_id, profile_id, external_user_id, source, current,
564
+ first_seen_at, last_seen_at, ended_at, raw_json, updated_at
565
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
566
+ on conflict(account_id, list_id, profile_id) do update set
567
+ external_user_id = coalesce(nullif(excluded.external_user_id, ''), x_list_members.external_user_id),
568
+ source = case when excluded.updated_at >= x_list_members.updated_at then excluded.source else x_list_members.source end,
569
+ current = case when excluded.updated_at >= x_list_members.updated_at then excluded.current else x_list_members.current end,
570
+ first_seen_at = min(x_list_members.first_seen_at, excluded.first_seen_at),
571
+ last_seen_at = max(x_list_members.last_seen_at, excluded.last_seen_at),
572
+ ended_at = case when excluded.updated_at >= x_list_members.updated_at then excluded.ended_at else x_list_members.ended_at end,
573
+ raw_json = case when excluded.updated_at >= x_list_members.updated_at then excluded.raw_json else x_list_members.raw_json end,
574
+ updated_at = max(x_list_members.updated_at, excluded.updated_at)
575
+ `,
576
+ columns: [
577
+ "account_id",
578
+ "list_id",
579
+ "profile_id",
580
+ "external_user_id",
581
+ "source",
582
+ "current",
583
+ "first_seen_at",
584
+ "last_seen_at",
585
+ "ended_at",
586
+ "raw_json",
587
+ "updated_at"
588
+ ]
589
+ }
590
+ },
490
591
  tweets: {
491
592
  exportSql: `
492
593
  select id, author_profile_id, text, created_at, is_replied, reply_to_id,
@@ -498,7 +599,7 @@ var definitions = {
498
599
  matchesPath: (candidate) => candidate.startsWith("data/tweets/"),
499
600
  countKey: () => "tweets",
500
601
  merge: {
501
- order: 9,
602
+ order: 11,
502
603
  transform: sanitizeImportedTweets,
503
604
  sql: `
504
605
  insert into tweets (
@@ -556,7 +657,7 @@ var definitions = {
556
657
  matchesPath: (candidate) => candidate.startsWith("data/collections/"),
557
658
  countKey: (candidate) => `collections_${pathLeaf(candidate).replace(/\.jsonl$/, "") || "unknown"}`,
558
659
  merge: {
559
- order: 10,
660
+ order: 12,
560
661
  sql: `
561
662
  insert into tweet_collections (
562
663
  account_id, tweet_id, kind, collected_at, source, raw_json, updated_at
@@ -595,7 +696,7 @@ var definitions = {
595
696
  matchesPath: (candidate) => candidate.startsWith("data/timeline_edges/"),
596
697
  countKey: (candidate) => `timeline_edges_${pathLeaf(candidate).replace(/\.jsonl$/, "") || "unknown"}`,
597
698
  merge: {
598
- order: 11,
699
+ order: 13,
599
700
  sql: `
600
701
  insert into tweet_account_edges (
601
702
  account_id, tweet_id, kind, first_seen_at, last_seen_at, seen_count,
@@ -634,7 +735,7 @@ var definitions = {
634
735
  `,
635
736
  ...fixedShard("data/dms/conversations.jsonl", "dm_conversations"),
636
737
  merge: {
637
- order: 12,
738
+ order: 14,
638
739
  sql: `
639
740
  insert into dm_conversations (
640
741
  id, account_id, participant_profile_id, title, inbox_kind, last_message_at, unread_count, needs_reply
@@ -675,7 +776,7 @@ var definitions = {
675
776
  matchesPath: (candidate) => candidate.startsWith("data/dms/") && candidate !== "data/dms/conversations.jsonl",
676
777
  countKey: () => "dm_messages",
677
778
  merge: {
678
- order: 13,
779
+ order: 15,
679
780
  sql: `
680
781
  insert into dm_messages (
681
782
  id, conversation_id, sender_profile_id, text, created_at, direction, is_replied, media_count
@@ -716,7 +817,7 @@ var definitions = {
716
817
  `,
717
818
  ...fixedShard("data/links/url_expansions.jsonl", "url_expansions"),
718
819
  merge: {
719
- order: 14,
820
+ order: 16,
720
821
  transform: sanitizeImportedUrlExpansions,
721
822
  sql: `
722
823
  insert into url_expansions (
@@ -764,7 +865,7 @@ var definitions = {
764
865
  `,
765
866
  ...fixedShard("data/links/occurrences.jsonl", "link_occurrences"),
766
867
  merge: {
767
- order: 15,
868
+ order: 17,
768
869
  sql: `
769
870
  insert into link_occurrences (
770
871
  source_kind, source_id, source_position, short_url, account_id,
@@ -796,7 +897,7 @@ var definitions = {
796
897
  `,
797
898
  ...fixedShard("data/moderation/blocks.jsonl", "blocks"),
798
899
  merge: {
799
- order: 16,
900
+ order: 18,
800
901
  sql: `
801
902
  insert into blocks (account_id, profile_id, source, created_at)
802
903
  values (?, ?, ?, ?)
@@ -815,7 +916,7 @@ var definitions = {
815
916
  `,
816
917
  ...fixedShard("data/moderation/mutes.jsonl", "mutes"),
817
918
  merge: {
818
- order: 17,
919
+ order: 19,
819
920
  sql: `
820
921
  insert into mutes (account_id, profile_id, source, created_at)
821
922
  values (?, ?, ?, ?)
@@ -834,7 +935,7 @@ var definitions = {
834
935
  `,
835
936
  ...fixedShard("data/actions/tweet_actions.jsonl", "tweet_actions"),
836
937
  merge: {
837
- order: 18,
938
+ order: 20,
838
939
  sql: `
839
940
  insert into tweet_actions (id, account_id, tweet_id, kind, body, created_at)
840
941
  values (?, ?, ?, ?, ?, ?)
@@ -856,7 +957,7 @@ var definitions = {
856
957
  `,
857
958
  ...fixedShard("data/ai_scores.jsonl", "ai_scores"),
858
959
  merge: {
859
- order: 19,
960
+ order: 21,
860
961
  sql: `
861
962
  insert into ai_scores (
862
963
  entity_kind, entity_id, model, score, summary, reasoning, updated_at
@@ -2402,6 +2503,43 @@ var BASE_SCHEMA_SQL = `
2402
2503
  snapshot_id text not null
2403
2504
  );
2404
2505
 
2506
+ create table if not exists x_lists (
2507
+ account_id text not null,
2508
+ list_id text not null,
2509
+ name text not null,
2510
+ description text not null default '',
2511
+ owner_profile_id text,
2512
+ owner_external_user_id text,
2513
+ is_private integer not null default 0,
2514
+ member_count integer,
2515
+ follower_count integer,
2516
+ source text not null,
2517
+ membership_status text not null default 'not_synced',
2518
+ lists_synced_at text not null,
2519
+ members_synced_at text,
2520
+ member_page_count integer not null default 0,
2521
+ member_result_count integer not null default 0,
2522
+ rate_limit_json text not null default '{}',
2523
+ raw_json text not null default '{}',
2524
+ updated_at text not null,
2525
+ primary key (account_id, list_id)
2526
+ );
2527
+
2528
+ create table if not exists x_list_members (
2529
+ account_id text not null,
2530
+ list_id text not null,
2531
+ profile_id text not null,
2532
+ external_user_id text not null,
2533
+ source text not null,
2534
+ current integer not null default 1,
2535
+ first_seen_at text not null,
2536
+ last_seen_at text not null,
2537
+ ended_at text,
2538
+ raw_json text not null default '{}',
2539
+ updated_at text not null,
2540
+ primary key (account_id, list_id, profile_id)
2541
+ );
2542
+
2405
2543
  create table if not exists geocoded_locations (
2406
2544
  normalized_key text primary key,
2407
2545
  original text not null,
@@ -2474,6 +2612,9 @@ var INDEX_SQL = `
2474
2612
  create index if not exists idx_follow_edges_profile on follow_edges(profile_id, current);
2475
2613
  create index if not exists idx_follow_snapshots_account on follow_snapshots(account_id, direction, completed_at desc);
2476
2614
  create index if not exists idx_follow_events_account on follow_events(account_id, direction, kind, event_at desc);
2615
+ create index if not exists idx_x_lists_account_name on x_lists(account_id, name collate nocase, list_id);
2616
+ create index if not exists idx_x_list_members_current on x_list_members(account_id, list_id, current, profile_id);
2617
+ create index if not exists idx_x_list_members_profile on x_list_members(profile_id, current, account_id, list_id);
2477
2618
  create index if not exists idx_geocoded_locations_country on geocoded_locations(country_code);
2478
2619
  create index if not exists idx_geocoded_locations_last_used on geocoded_locations(last_used_at desc);
2479
2620
  create index if not exists idx_geocoded_unresolved_ttl on geocoded_locations_unresolved(ttl_until desc);
@@ -2735,6 +2876,50 @@ function ensureFollowGraphTables(db) {
2735
2876
  );
2736
2877
  `);
2737
2878
  }
2879
+ function ensureXListTables(db) {
2880
+ db.exec(`
2881
+ create table if not exists x_lists (
2882
+ account_id text not null,
2883
+ list_id text not null,
2884
+ name text not null,
2885
+ description text not null default '',
2886
+ owner_profile_id text,
2887
+ owner_external_user_id text,
2888
+ is_private integer not null default 0,
2889
+ member_count integer,
2890
+ follower_count integer,
2891
+ source text not null,
2892
+ membership_status text not null default 'not_synced',
2893
+ lists_synced_at text not null,
2894
+ members_synced_at text,
2895
+ member_page_count integer not null default 0,
2896
+ member_result_count integer not null default 0,
2897
+ rate_limit_json text not null default '{}',
2898
+ raw_json text not null default '{}',
2899
+ updated_at text not null,
2900
+ primary key (account_id, list_id)
2901
+ );
2902
+
2903
+ create table if not exists x_list_members (
2904
+ account_id text not null,
2905
+ list_id text not null,
2906
+ profile_id text not null,
2907
+ external_user_id text not null,
2908
+ source text not null,
2909
+ current integer not null default 1,
2910
+ first_seen_at text not null,
2911
+ last_seen_at text not null,
2912
+ ended_at text,
2913
+ raw_json text not null default '{}',
2914
+ updated_at text not null,
2915
+ primary key (account_id, list_id, profile_id)
2916
+ );
2917
+
2918
+ create index if not exists idx_x_lists_account_name on x_lists(account_id, name collate nocase, list_id);
2919
+ create index if not exists idx_x_list_members_current on x_list_members(account_id, list_id, current, profile_id);
2920
+ create index if not exists idx_x_list_members_profile on x_list_members(profile_id, current, account_id, list_id);
2921
+ `);
2922
+ }
2738
2923
  function backfillTweetCollections(db) {
2739
2924
  const missingKinds = [
2740
2925
  ["likes", "liked"],
@@ -2880,6 +3065,13 @@ var DATABASE_MIGRATIONS = [
2880
3065
  version: 2,
2881
3066
  name: "normalize tweet account and collection state",
2882
3067
  up: normalizeTweetState
3068
+ },
3069
+ {
3070
+ version: 3,
3071
+ name: "add durable X List metadata and membership",
3072
+ up: (db) => {
3073
+ ensureXListTables(db);
3074
+ }
2883
3075
  }
2884
3076
  ];
2885
3077
  function ensureDemoData(db) {
@@ -3115,6 +3307,8 @@ var ImportRepository = class {
3115
3307
  }
3116
3308
  clearBackupImport() {
3117
3309
  this.db.exec(`
3310
+ delete from x_list_members;
3311
+ delete from x_lists;
3118
3312
  delete from follow_events;
3119
3313
  delete from follow_edges;
3120
3314
  delete from follow_snapshot_members;
@@ -3522,10 +3716,11 @@ function runSubprocessEffect(options) {
3522
3716
  }
3523
3717
 
3524
3718
  // src/lib/backup.ts
3525
- var BACKUP_SCHEMA_VERSION = 2;
3719
+ var BACKUP_SCHEMA_VERSION = 3;
3526
3720
  var MIN_SUPPORTED_BACKUP_SCHEMA_VERSION = 1;
3527
3721
  var MANIFEST_PATH = "manifest.json";
3528
3722
  var DATA_DIR = "data";
3723
+ var GITATTRIBUTES_PATH = ".gitattributes";
3529
3724
  var AUTO_SYNC_CACHE_KEY = "backup:auto-sync";
3530
3725
  var DEFAULT_STALE_AFTER_SECONDS = 15 * 60;
3531
3726
  var autoUpdateInFlight = null;
@@ -3750,6 +3945,8 @@ data/moderation/mutes.jsonl
3750
3945
  data/follow_snapshots.jsonl
3751
3946
  data/follow_snapshot_members.jsonl
3752
3947
  data/follow_edges.jsonl
3948
+ data/lists/lists.jsonl
3949
+ data/lists/members.jsonl
3753
3950
  data/follow_events.jsonl
3754
3951
  \`\`\`
3755
3952
 
@@ -3787,6 +3984,36 @@ function readPreviousManifestEffect(repoPath) {
3787
3984
  Effect5.catchAll(() => Effect5.succeed(void 0))
3788
3985
  );
3789
3986
  }
3987
+ function ensureBackupGitattributesEffect(repoPath) {
3988
+ return Effect5.gen(function* () {
3989
+ const attributesPath = yield* trySync2(
3990
+ () => resolveBackupFilePath(repoPath, GITATTRIBUTES_PATH)
3991
+ );
3992
+ yield* assertNoSymlinkAncestorEffect(repoPath, attributesPath);
3993
+ const requiredLines = [
3994
+ "data/**/*.jsonl text eol=lf",
3995
+ `${MANIFEST_PATH} text eol=lf`
3996
+ ];
3997
+ const generatedBlock = [
3998
+ "# BEGIN birdclaw backup attributes",
3999
+ "# Backup hashes use the raw LF-delimited bytes written by Birdclaw.",
4000
+ ...requiredLines,
4001
+ "# END birdclaw backup attributes",
4002
+ ""
4003
+ ].join("\n");
4004
+ const current = yield* tryPromise(
4005
+ () => fs.readFile(attributesPath, "utf8")
4006
+ ).pipe(Effect5.option);
4007
+ if (current._tag === "Some" && current.value.endsWith(generatedBlock)) {
4008
+ return;
4009
+ }
4010
+ const preserved = current._tag === "Some" ? current.value.replaceAll(generatedBlock, "").replace(/[\r\n]+$/u, "") : "";
4011
+ const content = preserved ? `${preserved}
4012
+
4013
+ ${generatedBlock}` : generatedBlock;
4014
+ yield* tryPromise(() => fs.writeFile(attributesPath, content, "utf8"));
4015
+ });
4016
+ }
3790
4017
  function maybeCommitAndPushEffect({
3791
4018
  repoPath,
3792
4019
  message,
@@ -3797,20 +4024,21 @@ function maybeCommitAndPushEffect({
3797
4024
  return Effect5.succeed(void 0);
3798
4025
  }
3799
4026
  return Effect5.gen(function* () {
3800
- yield* gitEffect([
3801
- "-C",
3802
- repoPath,
3803
- "rev-parse",
3804
- "--is-inside-work-tree"
3805
- ]).pipe(
3806
- Effect5.catchAll(
3807
- () => gitEffect(["-C", repoPath, "init"]).pipe(Effect5.asVoid)
3808
- )
3809
- );
4027
+ if (!(yield* isGitRepoEffect(repoPath))) {
4028
+ yield* gitEffect(["-C", repoPath, "init"]);
4029
+ }
4030
+ if (!(yield* isGitRepoEffect(repoPath))) {
4031
+ return yield* Effect5.fail(
4032
+ new Error(
4033
+ "Backup Git operations must run at the configured repository root"
4034
+ )
4035
+ );
4036
+ }
3810
4037
  yield* gitEffect([
3811
4038
  "-C",
3812
4039
  repoPath,
3813
4040
  "add",
4041
+ GITATTRIBUTES_PATH,
3814
4042
  "README.md",
3815
4043
  MANIFEST_PATH,
3816
4044
  DATA_DIR
@@ -3878,6 +4106,9 @@ function maybeCommitAndPushEffect({
3878
4106
  });
3879
4107
  }
3880
4108
  function isGitRepoEffect(repoPath) {
4109
+ if (!existsSync2(path2.join(repoPath, ".git"))) {
4110
+ return Effect5.succeed(false);
4111
+ }
3881
4112
  return gitEffect(["-C", repoPath, "rev-parse", "--is-inside-work-tree"]).pipe(
3882
4113
  Effect5.as(true),
3883
4114
  Effect5.catchAll(() => Effect5.succeed(false))
@@ -3934,7 +4165,7 @@ function ensureBackupGitRepoEffect({
3934
4165
  repoPath,
3935
4166
  "fetch",
3936
4167
  "origin",
3937
- "main"
4168
+ "main:refs/remotes/origin/main"
3938
4169
  ]).pipe(
3939
4170
  Effect5.flatMap(
3940
4171
  () => gitEffect(["-C", repoPath, "checkout", "-B", "main", "origin/main"])
@@ -3983,6 +4214,7 @@ function exportBackupEffect({
3983
4214
  new Error("Backup repository path must be a real directory")
3984
4215
  );
3985
4216
  }
4217
+ yield* ensureBackupGitattributesEffect(resolvedRepoPath);
3986
4218
  yield* ensureBackupReadmeEffect(resolvedRepoPath);
3987
4219
  const shards = yield* trySync2(() => buildShards(database));
3988
4220
  const shardEntries = yield* trySync2(
@@ -6133,6 +6365,16 @@ function runBirdJsonCommandAllowFailureEffect(args, timeoutMs) {
6133
6365
  })
6134
6366
  );
6135
6367
  }
6368
+ function runBirdTweetJsonCommandEffect(args, timeoutMs) {
6369
+ return runBirdJsonCommandEffect([...args, "--json-full"], timeoutMs).pipe(
6370
+ Effect8.catchAll((error) => {
6371
+ if (!isUnsupportedBirdOptionError(error, "--json-full")) {
6372
+ return Effect8.fail(error);
6373
+ }
6374
+ return runBirdJsonCommandEffect([...args, "--json"], timeoutMs);
6375
+ })
6376
+ );
6377
+ }
6136
6378
  function getBirdTweetItems(payload, command) {
6137
6379
  if (Array.isArray(payload)) {
6138
6380
  return payload;
@@ -6197,19 +6439,63 @@ function toReferencedTweets(item) {
6197
6439
  }
6198
6440
  return references.length > 0 ? references : void 0;
6199
6441
  }
6442
+ function getRecord(value) {
6443
+ return value && typeof value === "object" ? value : void 0;
6444
+ }
6445
+ function findGraphqlAvatarUrl(value, authorId) {
6446
+ const pending = [value];
6447
+ const seen = /* @__PURE__ */ new Set();
6448
+ while (pending.length > 0) {
6449
+ const current = pending.pop();
6450
+ const record3 = getRecord(current);
6451
+ if (!record3 || seen.has(record3)) continue;
6452
+ seen.add(record3);
6453
+ if (record3.rest_id === authorId) {
6454
+ const avatar = getRecord(record3.avatar);
6455
+ if (typeof avatar?.image_url === "string" && avatar.image_url) {
6456
+ return avatar.image_url;
6457
+ }
6458
+ }
6459
+ for (const child of Object.values(record3)) {
6460
+ if (child && typeof child === "object") {
6461
+ pending.push(child);
6462
+ }
6463
+ }
6464
+ }
6465
+ return void 0;
6466
+ }
6467
+ function isHydratedBirdTweetItem(value) {
6468
+ const item = value;
6469
+ return typeof item?.id === "string" && typeof item.text === "string" && typeof item.createdAt === "string";
6470
+ }
6200
6471
  function normalizeBirdTweets(items) {
6201
6472
  const users = /* @__PURE__ */ new Map();
6202
- const data = items.map((item) => {
6473
+ const includedTweets = /* @__PURE__ */ new Map();
6474
+ const normalizeItem = (item, preserveMissingMetrics = false) => {
6203
6475
  const authorId = String(
6204
6476
  item.authorId ?? item.author?.username ?? "unknown"
6205
6477
  );
6206
- if (!users.has(authorId)) {
6478
+ const profileImageUrl = item.author?.profileImageUrl ?? findGraphqlAvatarUrl(item._raw, authorId);
6479
+ const existingUser = users.get(authorId);
6480
+ if (!existingUser) {
6207
6481
  users.set(authorId, {
6208
6482
  id: authorId,
6209
6483
  username: item.author?.username ?? `user_${authorId}`,
6210
- name: item.author?.name ?? item.author?.username ?? `user_${authorId}`
6484
+ name: item.author?.name ?? item.author?.username ?? `user_${authorId}`,
6485
+ ...profileImageUrl ? { profile_image_url: profileImageUrl } : {}
6486
+ });
6487
+ } else if (profileImageUrl && !existingUser.profile_image_url) {
6488
+ users.set(authorId, {
6489
+ ...existingUser,
6490
+ profile_image_url: profileImageUrl
6211
6491
  });
6212
6492
  }
6493
+ if (isHydratedBirdTweetItem(item.quotedTweet)) {
6494
+ const quotedTweet = normalizeItem(item.quotedTweet, true);
6495
+ if (quotedTweet.id !== item.id) {
6496
+ includedTweets.set(quotedTweet.id, quotedTweet);
6497
+ }
6498
+ }
6213
6499
  return {
6214
6500
  id: item.id,
6215
6501
  author_id: authorId,
@@ -6218,17 +6504,29 @@ function normalizeBirdTweets(items) {
6218
6504
  conversation_id: item.conversationId ?? item.id,
6219
6505
  entities: toTweetEntities(item),
6220
6506
  referenced_tweets: toReferencedTweets(item),
6221
- public_metrics: {
6507
+ public_metrics: preserveMissingMetrics ? {
6508
+ ...item.replyCount === void 0 ? {} : { reply_count: Number(item.replyCount) },
6509
+ ...item.retweetCount === void 0 ? {} : { retweet_count: Number(item.retweetCount) },
6510
+ ...item.likeCount === void 0 ? {} : { like_count: Number(item.likeCount) }
6511
+ } : {
6222
6512
  reply_count: Number(item.replyCount ?? 0),
6223
6513
  retweet_count: Number(item.retweetCount ?? 0),
6224
6514
  like_count: Number(item.likeCount ?? 0)
6225
6515
  },
6226
6516
  edit_history_tweet_ids: [item.id]
6227
6517
  };
6228
- });
6518
+ };
6519
+ const data = items.map((item) => normalizeItem(item));
6520
+ const includes = {};
6521
+ if (users.size > 0) {
6522
+ includes.users = Array.from(users.values());
6523
+ }
6524
+ if (includedTweets.size > 0) {
6525
+ includes.tweets = Array.from(includedTweets.values());
6526
+ }
6229
6527
  return {
6230
6528
  data,
6231
- includes: users.size > 0 ? { users: Array.from(users.values()) } : void 0,
6529
+ includes: users.size > 0 || includedTweets.size > 0 ? includes : void 0,
6232
6530
  meta: {
6233
6531
  result_count: data.length,
6234
6532
  page_count: 1,
@@ -6260,11 +6558,10 @@ function listMentionsViaBirdEffect({
6260
6558
  maxResults
6261
6559
  }) {
6262
6560
  return Effect8.gen(function* () {
6263
- const stdout = yield* runBirdJsonCommandEffect([
6561
+ const stdout = yield* runBirdTweetJsonCommandEffect([
6264
6562
  "mentions",
6265
6563
  "-n",
6266
- String(maxResults),
6267
- "--json"
6564
+ String(maxResults)
6268
6565
  ]);
6269
6566
  const payload = yield* parseBirdJsonEffect(stdout);
6270
6567
  return yield* normalizeBirdTweetsPayloadEffect(payload, "mentions");
@@ -6277,14 +6574,14 @@ function listTweetsViaBirdCommandEffect({
6277
6574
  maxPages
6278
6575
  }) {
6279
6576
  return Effect8.gen(function* () {
6280
- const args = [command, "-n", String(maxResults), "--json"];
6577
+ const args = [command, "-n", String(maxResults)];
6281
6578
  if (all) {
6282
6579
  args.push("--all");
6580
+ if (maxPages !== void 0) {
6581
+ args.push("--max-pages", String(maxPages));
6582
+ }
6283
6583
  }
6284
- if (maxPages !== void 0) {
6285
- args.push("--max-pages", String(maxPages));
6286
- }
6287
- const stdout = yield* runBirdJsonCommandEffect(args);
6584
+ const stdout = yield* runBirdTweetJsonCommandEffect(args);
6288
6585
  const payload = yield* parseBirdJsonEffect(stdout);
6289
6586
  return yield* normalizeBirdTweetsPayloadEffect(payload, command);
6290
6587
  });
@@ -6303,14 +6600,14 @@ function listBookmarkedTweetsViaBirdEffect(options) {
6303
6600
  }
6304
6601
  function searchTweetsViaBirdEffect(query, options) {
6305
6602
  return Effect8.gen(function* () {
6306
- const args = ["search", query, "-n", String(options.maxResults), "--json"];
6603
+ const args = ["search", query, "-n", String(options.maxResults)];
6307
6604
  if (options.all) {
6308
6605
  args.push("--all");
6309
6606
  }
6310
6607
  if (options.all && options.maxPages !== void 0) {
6311
6608
  args.push("--max-pages", String(options.maxPages));
6312
6609
  }
6313
- const stdout = yield* runBirdJsonCommandEffect(args);
6610
+ const stdout = yield* runBirdTweetJsonCommandEffect(args);
6314
6611
  const payload = yield* parseBirdJsonEffect(stdout);
6315
6612
  return yield* normalizeBirdTweetsPayloadEffect(payload, "search");
6316
6613
  });
@@ -6323,11 +6620,7 @@ function lookupTweetsByIdsViaBirdEffect(ids) {
6323
6620
  const tweets = yield* Effect8.forEach(
6324
6621
  ids,
6325
6622
  (id) => Effect8.gen(function* () {
6326
- const stdout = yield* runBirdJsonCommandEffect([
6327
- "read",
6328
- id,
6329
- "--json"
6330
- ]);
6623
+ const stdout = yield* runBirdTweetJsonCommandEffect(["read", id]);
6331
6624
  const payload = yield* parseBirdJsonEffect(stdout);
6332
6625
  return yield* normalizeBirdTweetItemEffect(payload, "read");
6333
6626
  }),
@@ -6341,11 +6634,11 @@ function listHomeTimelineViaBirdEffect({
6341
6634
  following = true
6342
6635
  }) {
6343
6636
  return Effect8.gen(function* () {
6344
- const args = ["home", "-n", String(maxResults), "--json"];
6637
+ const args = ["home", "-n", String(maxResults)];
6345
6638
  if (following) {
6346
6639
  args.push("--following");
6347
6640
  }
6348
- const stdout = yield* runBirdJsonCommandEffect(args);
6641
+ const stdout = yield* runBirdTweetJsonCommandEffect(args);
6349
6642
  const payload = yield* parseBirdJsonEffect(stdout);
6350
6643
  return yield* normalizeBirdTweetsPayloadEffect(payload, "home");
6351
6644
  });
@@ -6358,12 +6651,14 @@ function normalizeBirdFollowUsers(payload, command, maxResults) {
6358
6651
  }
6359
6652
  const data = users.map(toXurlMentionUser).filter((user) => Boolean(user));
6360
6653
  const nextToken = !Array.isArray(rawPayload) && typeof rawPayload.nextCursor === "string" ? rawPayload.nextCursor : null;
6654
+ const paginationKnownComplete = Array.isArray(rawPayload) ? data.length < maxResults : nextToken === null;
6361
6655
  return {
6362
6656
  data,
6363
6657
  meta: {
6364
6658
  result_count: data.length,
6365
6659
  page_count: data.length > 0 ? Math.max(1, Math.ceil(data.length / maxResults)) : 1,
6366
- next_token: nextToken
6660
+ next_token: nextToken,
6661
+ pagination_known_complete: paginationKnownComplete
6367
6662
  }
6368
6663
  };
6369
6664
  }
@@ -6400,6 +6695,73 @@ function listFollowUsersViaBirdEffect({
6400
6695
  );
6401
6696
  });
6402
6697
  }
6698
+ function normalizeBirdLists(payload) {
6699
+ if (!Array.isArray(payload)) {
6700
+ throw new Error("bird lists returned unexpected JSON");
6701
+ }
6702
+ const data = payload.map((value) => {
6703
+ const item = value;
6704
+ if (!item.id || !item.name) return null;
6705
+ return {
6706
+ id: item.id,
6707
+ name: item.name,
6708
+ ...item.description ? { description: item.description } : {},
6709
+ ...typeof item.memberCount === "number" ? { memberCount: item.memberCount } : {},
6710
+ ...typeof item.subscriberCount === "number" ? { followerCount: item.subscriberCount } : {},
6711
+ ...typeof item.isPrivate === "boolean" ? { isPrivate: item.isPrivate } : {},
6712
+ ...item.owner?.id ? { ownerId: item.owner.id } : {},
6713
+ ...item.owner?.username ? { ownerUsername: item.owner.username } : {},
6714
+ ...item.owner?.name ? { ownerName: item.owner.name } : {},
6715
+ ...item.createdAt ? { createdAt: item.createdAt } : {},
6716
+ raw: value
6717
+ };
6718
+ }).filter((item) => Boolean(item));
6719
+ return { data, meta: { result_count: data.length, next_token: null } };
6720
+ }
6721
+ function listOwnedXListsViaBirdEffect({
6722
+ maxResults
6723
+ }) {
6724
+ return Effect8.gen(function* () {
6725
+ const stdout = yield* runBirdJsonCommandEffect([
6726
+ "lists",
6727
+ "-n",
6728
+ String(maxResults),
6729
+ "--json"
6730
+ ]);
6731
+ const payload = yield* parseBirdJsonEffect(stdout);
6732
+ return yield* Effect8.try({
6733
+ try: () => normalizeBirdLists(payload),
6734
+ catch: (error) => error
6735
+ });
6736
+ });
6737
+ }
6738
+ function listXListMembersViaBirdEffect({
6739
+ listId,
6740
+ maxResults,
6741
+ maxPages = 1
6742
+ }) {
6743
+ return Effect8.gen(function* () {
6744
+ const args = ["list-members", listId, "-n", String(maxResults), "--json"];
6745
+ if (maxPages > 1) {
6746
+ args.push("--all", "--max-pages", String(maxPages));
6747
+ }
6748
+ const stdout = yield* runBirdJsonCommandEffect(args);
6749
+ const payload = yield* parseBirdJsonEffect(stdout);
6750
+ const normalized = yield* normalizeBirdFollowUsersEffect(
6751
+ payload,
6752
+ "list-members",
6753
+ maxResults
6754
+ );
6755
+ if (Array.isArray(payload)) {
6756
+ normalized.meta = {
6757
+ ...normalized.meta,
6758
+ pagination_known_complete: false,
6759
+ pagination_inferred_complete: normalized.data.length < maxResults
6760
+ };
6761
+ }
6762
+ return normalized;
6763
+ });
6764
+ }
6403
6765
  function listThreadViaBirdEffect({
6404
6766
  tweetId,
6405
6767
  all,
@@ -6407,14 +6769,14 @@ function listThreadViaBirdEffect({
6407
6769
  timeoutMs
6408
6770
  }) {
6409
6771
  return Effect8.gen(function* () {
6410
- const args = ["thread", tweetId, "--json"];
6772
+ const args = ["thread", tweetId];
6411
6773
  if (all) {
6412
6774
  args.push("--all");
6413
6775
  }
6414
6776
  if (maxPages !== void 0) {
6415
6777
  args.push("--max-pages", String(maxPages));
6416
6778
  }
6417
- const stdout = yield* runBirdJsonCommandEffect(args, timeoutMs);
6779
+ const stdout = yield* runBirdTweetJsonCommandEffect(args, timeoutMs);
6418
6780
  const payload = yield* parseBirdJsonEffect(stdout);
6419
6781
  return yield* normalizeBirdTweetsPayloadEffect(payload, "thread");
6420
6782
  });
@@ -7104,6 +7466,71 @@ function lookupUsersByIdsEffect(ids) {
7104
7466
  function lookupUsersByIds(ids) {
7105
7467
  return runEffectPromise(lookupUsersByIdsEffect(ids));
7106
7468
  }
7469
+ function normalizeXListPage(payload) {
7470
+ const rawItems = Array.isArray(payload.data) ? payload.data : [];
7471
+ const data = rawItems.map((value) => {
7472
+ const item = value;
7473
+ if (typeof item.id !== "string" || typeof item.name !== "string") {
7474
+ return null;
7475
+ }
7476
+ return {
7477
+ id: item.id,
7478
+ name: item.name,
7479
+ ...typeof item.description === "string" ? { description: item.description } : {},
7480
+ ...typeof item.member_count === "number" ? { memberCount: item.member_count } : {},
7481
+ ...typeof item.follower_count === "number" ? { followerCount: item.follower_count } : {},
7482
+ ...typeof item.private === "boolean" ? { isPrivate: item.private } : {},
7483
+ ...typeof item.owner_id === "string" ? { ownerId: item.owner_id } : {},
7484
+ ...typeof item.created_at === "string" ? { createdAt: item.created_at } : {},
7485
+ raw: item
7486
+ };
7487
+ }).filter((item) => Boolean(item));
7488
+ const meta = payload.meta && typeof payload.meta === "object" ? payload.meta : {};
7489
+ return {
7490
+ data,
7491
+ meta: {
7492
+ result_count: data.length,
7493
+ ...typeof meta.next_token === "string" ? { next_token: meta.next_token } : { next_token: null }
7494
+ }
7495
+ };
7496
+ }
7497
+ function listOwnedXListsViaXurlEffect({
7498
+ userId,
7499
+ username,
7500
+ maxResults,
7501
+ paginationToken
7502
+ }) {
7503
+ const query = new URLSearchParams({
7504
+ max_results: String(maxResults),
7505
+ "list.fields": "created_at,description,follower_count,member_count,name,owner_id,private"
7506
+ });
7507
+ if (paginationToken) query.set("pagination_token", paginationToken);
7508
+ return runOAuth2JsonCommandEffect({
7509
+ args: [`/2/users/${userId}/owned_lists?${query.toString()}`],
7510
+ username
7511
+ }).pipe(Effect9.map(normalizeXListPage));
7512
+ }
7513
+ function listXListMembersViaXurlEffect({
7514
+ listId,
7515
+ username,
7516
+ maxResults,
7517
+ paginationToken
7518
+ }) {
7519
+ const query = new URLSearchParams({
7520
+ max_results: String(maxResults),
7521
+ "user.fields": RICH_USER_FIELDS
7522
+ });
7523
+ if (paginationToken) query.set("pagination_token", paginationToken);
7524
+ return runOAuth2JsonCommandEffect({
7525
+ args: [`/2/lists/${listId}/members?${query.toString()}`],
7526
+ username
7527
+ }).pipe(
7528
+ Effect9.map((payload) => ({
7529
+ data: Array.isArray(payload.data) ? payload.data : [],
7530
+ meta: payload.meta && typeof payload.meta === "object" ? payload.meta : {}
7531
+ }))
7532
+ );
7533
+ }
7107
7534
  function lookupUsersByHandlesEffect(handles, options = {}) {
7108
7535
  if (handles.length === 0) {
7109
7536
  return Effect9.succeed([]);
@@ -8218,16 +8645,15 @@ function enrichFallbackUrlEntities(text, entities, resolveExpansion) {
8218
8645
  const existingUrls = entities.urls ?? [];
8219
8646
  const enrichedExistingUrls = existingUrls.map((entry) => {
8220
8647
  const expansion = resolveExpansion?.(entry.url);
8221
- if (!expansion) return entry;
8222
- const expandedUrl = expansion.expandedUrl || entry.expandedUrl;
8648
+ const expandedUrl = expansion?.expandedUrl || entry.expandedUrl || entry.url;
8223
8649
  return {
8224
8650
  ...entry,
8225
8651
  expandedUrl,
8226
- displayUrl: expansion.displayUrl || displayUrlForLink(expandedUrl),
8227
- ...expansion.title ? { title: expansion.title } : {},
8652
+ displayUrl: expansion?.displayUrl || entry.displayUrl || displayUrlForLink(expandedUrl),
8653
+ ...expansion?.title ? { title: expansion.title } : {},
8228
8654
  ...expansion && "description" in expansion ? { description: expansion.description ?? null } : {},
8229
- ...expansion.imageUrl ? { imageUrl: expansion.imageUrl } : {},
8230
- ...expansion.siteName ? { siteName: expansion.siteName } : {}
8655
+ ...expansion?.imageUrl ? { imageUrl: expansion.imageUrl } : {},
8656
+ ...expansion?.siteName ? { siteName: expansion.siteName } : {}
8231
8657
  };
8232
8658
  });
8233
8659
  const fallbackUrls = [];
@@ -9661,6 +10087,16 @@ function ensureStubProfileForXUser(db, externalUserId) {
9661
10087
  function getReferencedTweetId(tweet, type) {
9662
10088
  return tweet.referenced_tweets?.find((item) => item.type === type)?.id ?? null;
9663
10089
  }
10090
+ function toCanonicalTweets(payload) {
10091
+ const tweetsById = /* @__PURE__ */ new Map();
10092
+ for (const tweet of payload.includes?.tweets ?? []) {
10093
+ tweetsById.set(tweet.id, tweet);
10094
+ }
10095
+ for (const tweet of payload.data) {
10096
+ tweetsById.set(tweet.id, tweet);
10097
+ }
10098
+ return tweetsById.values();
10099
+ }
9664
10100
  function replaceTweetFts(db, tweetId, text) {
9665
10101
  db.prepare("delete from tweets_fts where tweet_id = ?").run(tweetId);
9666
10102
  db.prepare("insert into tweets_fts (tweet_id, text) values (?, ?)").run(
@@ -9690,7 +10126,7 @@ function ingestTweetPayload(db, {
9690
10126
  created_at = excluded.created_at,
9691
10127
  is_replied = max(tweets.is_replied, excluded.is_replied),
9692
10128
  reply_to_id = coalesce(tweets.reply_to_id, excluded.reply_to_id),
9693
- like_count = excluded.like_count,
10129
+ like_count = case when ? then tweets.like_count else excluded.like_count end,
9694
10130
  media_count = max(tweets.media_count, excluded.media_count),
9695
10131
  entities_json = excluded.entities_json,
9696
10132
  media_json = case
@@ -9711,25 +10147,29 @@ function ingestTweetPayload(db, {
9711
10147
  const tweetIds = [];
9712
10148
  db.transaction(() => {
9713
10149
  const observedAt = (/* @__PURE__ */ new Date()).toISOString();
9714
- for (const tweet of payload.data) {
10150
+ const primaryTweetIds = new Set(payload.data.map((tweet) => tweet.id));
10151
+ for (const tweet of toCanonicalTweets(payload)) {
10152
+ const isPrimaryTweet = primaryTweetIds.has(tweet.id);
9715
10153
  const author = usersById.get(tweet.author_id);
9716
10154
  const profile = author ? upsertProfileFromXUser(db, author) : ensureStubProfileForXUser(db, tweet.author_id);
9717
10155
  const replyToId = getReferencedTweetId(tweet, "replied_to");
9718
10156
  const quotedTweetId = getReferencedTweetId(tweet, "quoted");
10157
+ const shouldMarkReplied = isPrimaryTweet && markRepliesAsReplied && Boolean(replyToId);
9719
10158
  upsertTweet.run(
9720
10159
  tweet.id,
9721
10160
  profile.profile.id,
9722
10161
  tweet.text,
9723
10162
  tweet.created_at,
9724
- markRepliesAsReplied && replyToId ? 1 : 0,
10163
+ shouldMarkReplied ? 1 : 0,
9725
10164
  replyToId,
9726
10165
  Number(tweet.public_metrics?.like_count ?? 0),
9727
10166
  countTweetMedia(tweet),
9728
10167
  JSON.stringify(tweetEntitiesFromXurl(tweet.entities)),
9729
10168
  buildMediaJsonFromIncludes(tweet, payload.includes?.media),
9730
- quotedTweetId
10169
+ quotedTweetId,
10170
+ !isPrimaryTweet && tweet.public_metrics?.like_count === void 0 ? 1 : 0
9731
10171
  );
9732
- if (edgeKind) {
10172
+ if (edgeKind && isPrimaryTweet) {
9733
10173
  upsertTweetAccountEdge(db, {
9734
10174
  accountId,
9735
10175
  tweetId: tweet.id,
@@ -9739,16 +10179,20 @@ function ingestTweetPayload(db, {
9739
10179
  rawJson: JSON.stringify(tweet)
9740
10180
  });
9741
10181
  }
9742
- upsertCollection?.run(
9743
- accountId,
9744
- tweet.id,
9745
- collectionKind2,
9746
- source,
9747
- JSON.stringify(tweet),
9748
- observedAt
9749
- );
10182
+ if (isPrimaryTweet) {
10183
+ upsertCollection?.run(
10184
+ accountId,
10185
+ tweet.id,
10186
+ collectionKind2,
10187
+ source,
10188
+ JSON.stringify(tweet),
10189
+ observedAt
10190
+ );
10191
+ }
9750
10192
  replaceTweetFts(db, tweet.id, tweet.text);
9751
- tweetIds.push(tweet.id);
10193
+ if (isPrimaryTweet) {
10194
+ tweetIds.push(tweet.id);
10195
+ }
9752
10196
  }
9753
10197
  })();
9754
10198
  return tweetIds;
@@ -10720,6 +11164,8 @@ var RECENT_TIMELINE_EDGE_CANDIDATES = 5e3;
10720
11164
  function listTimelineItems({
10721
11165
  resource,
10722
11166
  account,
11167
+ listAccountId,
11168
+ listId,
10723
11169
  search,
10724
11170
  replyFilter = "all",
10725
11171
  since,
@@ -10750,14 +11196,14 @@ function listTimelineItems({
10750
11196
  let join3 = "";
10751
11197
  let where = "where e.kind = ?";
10752
11198
  let searchSnippetSelect = "";
10753
- const canUseRecentEdgeWindow = !likedOnly && !bookmarkedOnly && !account && !search?.trim() && replyFilter === "all" && !since?.trim() && !until?.trim() && includeReplies && qualityFilter === "all";
11199
+ const canUseRecentEdgeWindow = !likedOnly && !bookmarkedOnly && !listId && !account && !search?.trim() && replyFilter === "all" && !since?.trim() && !until?.trim() && includeReplies && qualityFilter === "all";
10754
11200
  if (likedOnly || bookmarkedOnly) {
10755
11201
  if (likedOnly && bookmarkedOnly) {
10756
11202
  timelineEdgesCte = `
10757
11203
  with timeline_edges as (
10758
11204
  select likes.account_id, likes.tweet_id, 'home' as kind, likes.raw_json
10759
- from tweet_collections likes
10760
- join tweet_collections bookmarks
11205
+ from tweet_collections likes indexed by idx_tweet_collections_tweet
11206
+ join tweet_collections bookmarks indexed by idx_tweet_collections_tweet
10761
11207
  on bookmarks.account_id = likes.account_id
10762
11208
  and bookmarks.tweet_id = likes.tweet_id
10763
11209
  and bookmarks.kind = 'bookmarks'
@@ -10769,7 +11215,7 @@ function listTimelineItems({
10769
11215
  timelineEdgesCte = `
10770
11216
  with timeline_edges as (
10771
11217
  select account_id, tweet_id, 'home' as kind, raw_json
10772
- from tweet_collections
11218
+ from tweet_collections indexed by idx_tweet_collections_tweet
10773
11219
  where kind = ?
10774
11220
  )
10775
11221
  `;
@@ -10846,6 +11292,22 @@ function listTimelineItems({
10846
11292
  params.push(until.trim());
10847
11293
  }
10848
11294
  }
11295
+ if (Boolean(listId) !== Boolean(listAccountId)) {
11296
+ throw new Error("List filtering requires both listId and listAccountId");
11297
+ }
11298
+ if (listId && listAccountId) {
11299
+ where += `
11300
+ and exists (
11301
+ select 1
11302
+ from x_list_members list_member
11303
+ where list_member.account_id = ?
11304
+ and list_member.list_id = ?
11305
+ and list_member.profile_id = t.author_profile_id
11306
+ and list_member.current = 1
11307
+ )
11308
+ `;
11309
+ params.push(listAccountId, listId);
11310
+ }
10849
11311
  const ftsSearch = search?.trim() ? toFtsSearchQuery(search) : "";
10850
11312
  if (ftsSearch) {
10851
11313
  join3 += " join tweets_fts on tweets_fts.tweet_id = t.id ";
@@ -12519,6 +12981,7 @@ function reachedStartTimeBoundary(payload, startTimeMs) {
12519
12981
  function mergeTimelinePayloads(payloads, limit) {
12520
12982
  const data = [];
12521
12983
  const usersById = /* @__PURE__ */ new Map();
12984
+ const tweetsById = /* @__PURE__ */ new Map();
12522
12985
  const mediaByKey = /* @__PURE__ */ new Map();
12523
12986
  let meta;
12524
12987
  for (const payload of payloads) {
@@ -12531,6 +12994,9 @@ function mergeTimelinePayloads(payloads, limit) {
12531
12994
  for (const user of payload.includes?.users ?? []) {
12532
12995
  usersById.set(user.id, user);
12533
12996
  }
12997
+ for (const tweet of payload.includes?.tweets ?? []) {
12998
+ tweetsById.set(tweet.id, tweet);
12999
+ }
12534
13000
  for (const media of payload.includes?.media ?? []) {
12535
13001
  mediaByKey.set(media.media_key, media);
12536
13002
  }
@@ -12540,6 +13006,7 @@ function mergeTimelinePayloads(payloads, limit) {
12540
13006
  data,
12541
13007
  includes: {
12542
13008
  users: [...usersById.values()],
13009
+ tweets: [...tweetsById.values()],
12543
13010
  media: [...mediaByKey.values()]
12544
13011
  },
12545
13012
  meta
@@ -12848,6 +13315,7 @@ function compactTweet(source, item) {
12848
13315
  createdAt: item.createdAt,
12849
13316
  text: item.text,
12850
13317
  entities: item.entities,
13318
+ media: item.media,
12851
13319
  likeCount: item.likeCount,
12852
13320
  liked: item.liked,
12853
13321
  bookmarked: item.bookmarked,
@@ -12867,6 +13335,7 @@ function compactEmbeddedTweet(item) {
12867
13335
  createdAt: item.createdAt,
12868
13336
  text: item.text,
12869
13337
  entities: item.entities,
13338
+ media: item.media,
12870
13339
  likeCount: item.likeCount ?? 0,
12871
13340
  liked: Boolean(item.liked),
12872
13341
  bookmarked: Boolean(item.bookmarked),
@@ -15391,6 +15860,8 @@ function compactTweet2(source, item) {
15391
15860
  authorProfile: item.author,
15392
15861
  createdAt: item.createdAt,
15393
15862
  text: item.text,
15863
+ entities: item.entities,
15864
+ media: item.media,
15394
15865
  likeCount: item.likeCount,
15395
15866
  liked: item.liked,
15396
15867
  bookmarked: item.bookmarked,
@@ -15428,7 +15899,9 @@ function collectLiveSearchTweets(options) {
15428
15899
  t.created_at,
15429
15900
  t.is_replied,
15430
15901
  t.like_count,
15431
- t.media_count,
15902
+ t.media_count,
15903
+ t.entities_json,
15904
+ t.media_json,
15432
15905
  case
15433
15906
  when exists (
15434
15907
  select 1 from tweet_collections collection
@@ -15482,6 +15955,8 @@ function collectLiveSearchTweets(options) {
15482
15955
  },
15483
15956
  createdAt: String(row.created_at),
15484
15957
  text: String(row.text),
15958
+ entities: parseJsonField2(row.entities_json, {}),
15959
+ media: parseJsonField2(row.media_json, []),
15485
15960
  likeCount: Number(row.like_count),
15486
15961
  liked: Boolean(row.liked),
15487
15962
  bookmarked: Boolean(row.bookmarked),
@@ -23372,19 +23847,550 @@ function registerJobCommands({ program: program2, print: print2 }) {
23372
23847
  });
23373
23848
  }
23374
23849
 
23850
+ // src/lib/x-lists.ts
23851
+ import { Effect as Effect47 } from "effect";
23852
+ var DEFAULT_MAX_LISTS = 20;
23853
+ var DEFAULT_MEMBER_LIMIT = 20;
23854
+ var DEFAULT_MAX_MEMBER_PAGES = 1;
23855
+ var DEFAULT_DELAY_MS2 = 1e3;
23856
+ var MAX_LISTS = 100;
23857
+ var MAX_MEMBER_LIMIT = 100;
23858
+ var MAX_MEMBER_PAGES = 100;
23859
+ function positiveInteger(name, value, maximum) {
23860
+ if (!Number.isFinite(value) || value < 1 || value > maximum) {
23861
+ throw new Error(`${name} must be between 1 and ${String(maximum)}`);
23862
+ }
23863
+ return Math.floor(value);
23864
+ }
23865
+ function nonNegativeInteger(name, value) {
23866
+ if (!Number.isFinite(value) || value < 0) {
23867
+ throw new Error(`${name} must be a non-negative integer`);
23868
+ }
23869
+ return Math.floor(value);
23870
+ }
23871
+ function normalizedMode(value) {
23872
+ if (value === void 0) return "auto";
23873
+ if (value === "auto" || value === "bird" || value === "xurl") return value;
23874
+ throw new Error("--mode must be auto, bird, or xurl");
23875
+ }
23876
+ function metaString(meta, key) {
23877
+ const value = meta?.[key];
23878
+ return typeof value === "string" && value.length > 0 ? value : void 0;
23879
+ }
23880
+ function metaNumber(meta, key) {
23881
+ const value = meta?.[key];
23882
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
23883
+ }
23884
+ function parseJsonRecord(value) {
23885
+ if (typeof value !== "string" || value.length === 0) return {};
23886
+ try {
23887
+ const parsed = JSON.parse(value);
23888
+ return parsed && typeof parsed === "object" ? parsed : {};
23889
+ } catch {
23890
+ return {};
23891
+ }
23892
+ }
23893
+ function fetchOwnedListsEffect({
23894
+ source,
23895
+ account,
23896
+ maxLists
23897
+ }) {
23898
+ if (source === "bird") {
23899
+ return Effect47.gen(function* () {
23900
+ const authenticated = yield* getAuthenticatedBirdAccountEffect();
23901
+ yield* trySync(
23902
+ () => assertLiveAccountMatches({
23903
+ source: "bird",
23904
+ account,
23905
+ liveUsername: authenticated.username,
23906
+ liveExternalUserId: authenticated.id
23907
+ })
23908
+ );
23909
+ return yield* listOwnedXListsViaBirdEffect({ maxResults: maxLists });
23910
+ });
23911
+ }
23912
+ return Effect47.gen(function* () {
23913
+ const authenticated = yield* lookupAuthenticatedOAuth2UserEffect(
23914
+ account.username
23915
+ );
23916
+ if (!authenticated) {
23917
+ return yield* Effect47.fail(
23918
+ new Error("xurl did not return an OAuth2 user")
23919
+ );
23920
+ }
23921
+ const liveUsername = String(authenticated.username ?? "");
23922
+ const liveExternalUserId = String(authenticated.id ?? "");
23923
+ yield* trySync(
23924
+ () => assertLiveAccountMatches({
23925
+ source: "xurl",
23926
+ account,
23927
+ liveUsername,
23928
+ liveExternalUserId
23929
+ })
23930
+ );
23931
+ const userId = account.externalUserId ?? liveExternalUserId;
23932
+ if (!userId) {
23933
+ return yield* Effect47.fail(
23934
+ new Error("xurl authenticated user is missing an id")
23935
+ );
23936
+ }
23937
+ return yield* listOwnedXListsViaXurlEffect({
23938
+ userId,
23939
+ username: account.username,
23940
+ maxResults: maxLists
23941
+ });
23942
+ });
23943
+ }
23944
+ function fetchListMembersEffect({
23945
+ source,
23946
+ list,
23947
+ username,
23948
+ memberLimit,
23949
+ maxMemberPages
23950
+ }) {
23951
+ if (source === "bird") {
23952
+ return listXListMembersViaBirdEffect({
23953
+ listId: list.id,
23954
+ maxResults: memberLimit,
23955
+ maxPages: maxMemberPages
23956
+ });
23957
+ }
23958
+ return Effect47.gen(function* () {
23959
+ const data = [];
23960
+ const seen = /* @__PURE__ */ new Set();
23961
+ let paginationToken;
23962
+ let pageCount = 0;
23963
+ do {
23964
+ const page = yield* listXListMembersViaXurlEffect({
23965
+ listId: list.id,
23966
+ username,
23967
+ maxResults: memberLimit,
23968
+ paginationToken
23969
+ });
23970
+ pageCount += 1;
23971
+ for (const user of page.data) {
23972
+ if (seen.has(user.id)) continue;
23973
+ seen.add(user.id);
23974
+ data.push(user);
23975
+ }
23976
+ paginationToken = metaString(page.meta, "next_token");
23977
+ } while (paginationToken && pageCount < maxMemberPages);
23978
+ return {
23979
+ data,
23980
+ meta: {
23981
+ result_count: data.length,
23982
+ page_count: pageCount,
23983
+ next_token: paginationToken ?? null,
23984
+ pagination_known_complete: !paginationToken
23985
+ }
23986
+ };
23987
+ });
23988
+ }
23989
+ function membershipStatus({
23990
+ list,
23991
+ payload
23992
+ }) {
23993
+ if (payload.meta?.pagination_known_complete === true) return "complete";
23994
+ if (payload.meta?.pagination_inferred_complete === true) return "inferred";
23995
+ if (typeof list.memberCount === "number" && payload.data.length >= list.memberCount) {
23996
+ return "inferred";
23997
+ }
23998
+ return "partial";
23999
+ }
24000
+ function ownerProfileId(db, list) {
24001
+ if (!list.ownerId || !list.ownerUsername) return void 0;
24002
+ return upsertProfileFromXUser(db, {
24003
+ id: list.ownerId,
24004
+ username: list.ownerUsername,
24005
+ name: list.ownerName ?? list.ownerUsername
24006
+ }).profile.id;
24007
+ }
24008
+ function persistListResult({
24009
+ db,
24010
+ accountId,
24011
+ list,
24012
+ source,
24013
+ payload,
24014
+ status,
24015
+ now: now2,
24016
+ rateLimit
24017
+ }) {
24018
+ return db.transaction(() => {
24019
+ const listOwnerProfileId = ownerProfileId(db, list);
24020
+ const membersSyncedAt = payload ? now2 : null;
24021
+ const pageCount = payload ? metaNumber(payload.meta, "page_count") ?? 1 : 0;
24022
+ const resultCount = payload?.data.length ?? 0;
24023
+ db.prepare(
24024
+ `
24025
+ insert into x_lists (
24026
+ account_id, list_id, name, description, owner_profile_id,
24027
+ owner_external_user_id, is_private, member_count, follower_count,
24028
+ source, membership_status, lists_synced_at, members_synced_at,
24029
+ member_page_count, member_result_count, rate_limit_json, raw_json,
24030
+ updated_at
24031
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
24032
+ on conflict(account_id, list_id) do update set
24033
+ name = excluded.name,
24034
+ description = excluded.description,
24035
+ owner_profile_id = coalesce(excluded.owner_profile_id, x_lists.owner_profile_id),
24036
+ owner_external_user_id = coalesce(excluded.owner_external_user_id, x_lists.owner_external_user_id),
24037
+ is_private = excluded.is_private,
24038
+ member_count = coalesce(excluded.member_count, x_lists.member_count),
24039
+ follower_count = coalesce(excluded.follower_count, x_lists.follower_count),
24040
+ source = excluded.source,
24041
+ membership_status = excluded.membership_status,
24042
+ lists_synced_at = excluded.lists_synced_at,
24043
+ members_synced_at = coalesce(excluded.members_synced_at, x_lists.members_synced_at),
24044
+ member_page_count = excluded.member_page_count,
24045
+ member_result_count = excluded.member_result_count,
24046
+ rate_limit_json = excluded.rate_limit_json,
24047
+ raw_json = excluded.raw_json,
24048
+ updated_at = excluded.updated_at
24049
+ `
24050
+ ).run(
24051
+ accountId,
24052
+ list.id,
24053
+ list.name,
24054
+ list.description ?? "",
24055
+ listOwnerProfileId ?? null,
24056
+ list.ownerId ?? null,
24057
+ list.isPrivate ? 1 : 0,
24058
+ list.memberCount ?? null,
24059
+ list.followerCount ?? null,
24060
+ source,
24061
+ status,
24062
+ now2,
24063
+ membersSyncedAt,
24064
+ pageCount,
24065
+ resultCount,
24066
+ JSON.stringify(rateLimit),
24067
+ JSON.stringify(list.raw ?? {}),
24068
+ now2
24069
+ );
24070
+ if (!payload) return;
24071
+ if (status === "complete") {
24072
+ db.prepare(
24073
+ `
24074
+ update x_list_members
24075
+ set current = 0, ended_at = ?, updated_at = ?
24076
+ where account_id = ? and list_id = ? and current = 1
24077
+ `
24078
+ ).run(now2, now2, accountId, list.id);
24079
+ }
24080
+ const upsertMember = db.prepare(`
24081
+ insert into x_list_members (
24082
+ account_id, list_id, profile_id, external_user_id, source, current,
24083
+ first_seen_at, last_seen_at, ended_at, raw_json, updated_at
24084
+ ) values (?, ?, ?, ?, ?, 1, ?, ?, null, ?, ?)
24085
+ on conflict(account_id, list_id, profile_id) do update set
24086
+ external_user_id = excluded.external_user_id,
24087
+ source = excluded.source,
24088
+ current = 1,
24089
+ last_seen_at = excluded.last_seen_at,
24090
+ ended_at = null,
24091
+ raw_json = excluded.raw_json,
24092
+ updated_at = excluded.updated_at
24093
+ `);
24094
+ for (const user of payload.data) {
24095
+ const resolved = upsertProfileFromXUser(db, user);
24096
+ upsertMember.run(
24097
+ accountId,
24098
+ list.id,
24099
+ resolved.profile.id,
24100
+ resolved.externalUserId,
24101
+ source,
24102
+ now2,
24103
+ now2,
24104
+ JSON.stringify(user),
24105
+ now2
24106
+ );
24107
+ }
24108
+ })();
24109
+ }
24110
+ function syncXListsEffect(options = {}) {
24111
+ return Effect47.suspend(() => {
24112
+ const mode = normalizedMode(options.mode);
24113
+ const maxLists = positiveInteger(
24114
+ "--max-lists",
24115
+ options.maxLists ?? DEFAULT_MAX_LISTS,
24116
+ MAX_LISTS
24117
+ );
24118
+ const memberLimit = positiveInteger(
24119
+ "--member-limit",
24120
+ options.memberLimit ?? DEFAULT_MEMBER_LIMIT,
24121
+ MAX_MEMBER_LIMIT
24122
+ );
24123
+ const maxMemberPages = positiveInteger(
24124
+ "--max-member-pages",
24125
+ options.maxMemberPages ?? DEFAULT_MAX_MEMBER_PAGES,
24126
+ MAX_MEMBER_PAGES
24127
+ );
24128
+ const delayMs = nonNegativeInteger(
24129
+ "--delay-ms",
24130
+ options.delayMs ?? DEFAULT_DELAY_MS2
24131
+ );
24132
+ const db = getNativeDb();
24133
+ const account = resolveLiveSyncAccount(db, options.account);
24134
+ const sources = mode === "auto" ? ["bird", "xurl"] : [mode];
24135
+ const transports = sources.map(
24136
+ (source) => createLiveTransportAdapter(
24137
+ source,
24138
+ fetchOwnedListsEffect({ source, account, maxLists })
24139
+ )
24140
+ );
24141
+ return Effect47.gen(function* () {
24142
+ const discovered = yield* fetchWithTransportFallbackEffect(transports);
24143
+ const source = discovered.source;
24144
+ const lists = discovered.payload.data.slice(0, maxLists);
24145
+ const results = [];
24146
+ for (const [index, list] of lists.entries()) {
24147
+ if (index > 0 && delayMs > 0) yield* Effect47.sleep(delayMs);
24148
+ const fetched = yield* fetchListMembersEffect({
24149
+ source,
24150
+ list,
24151
+ username: account.username,
24152
+ memberLimit,
24153
+ maxMemberPages
24154
+ }).pipe(
24155
+ Effect47.map((payload) => ({ ok: true, payload })),
24156
+ Effect47.catchAll(
24157
+ (error) => Effect47.succeed({ ok: false, error: toError(error) })
24158
+ )
24159
+ );
24160
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
24161
+ const rateLimit = {
24162
+ memberLimit,
24163
+ maxMemberPages,
24164
+ delayMs,
24165
+ nextCursorStored: fetched.ok ? Boolean(metaString(fetched.payload.meta, "next_token")) : false
24166
+ };
24167
+ if (!fetched.ok) {
24168
+ yield* databaseWriteEffect(
24169
+ (writeDb) => persistListResult({
24170
+ db: writeDb,
24171
+ accountId: account.accountId,
24172
+ list,
24173
+ source,
24174
+ status: "error",
24175
+ now: now2,
24176
+ rateLimit
24177
+ })
24178
+ );
24179
+ results.push({
24180
+ listId: list.id,
24181
+ name: list.name,
24182
+ status: "error",
24183
+ error: fetched.error.message
24184
+ });
24185
+ continue;
24186
+ }
24187
+ const status = membershipStatus({ list, payload: fetched.payload });
24188
+ yield* databaseWriteEffect(
24189
+ (writeDb) => persistListResult({
24190
+ db: writeDb,
24191
+ accountId: account.accountId,
24192
+ list,
24193
+ source,
24194
+ payload: fetched.payload,
24195
+ status,
24196
+ now: now2,
24197
+ rateLimit
24198
+ })
24199
+ );
24200
+ results.push({
24201
+ listId: list.id,
24202
+ name: list.name,
24203
+ status,
24204
+ members: fetched.payload.data.length,
24205
+ pages: metaNumber(fetched.payload.meta, "page_count") ?? 1
24206
+ });
24207
+ }
24208
+ const errors = results.filter((result) => result.status === "error");
24209
+ return {
24210
+ ok: errors.length === 0,
24211
+ accountId: account.accountId,
24212
+ mode,
24213
+ source,
24214
+ listCount: lists.length,
24215
+ membershipCompleteCount: results.filter(
24216
+ (result) => result.status === "complete"
24217
+ ).length,
24218
+ membershipPartialCount: results.filter(
24219
+ (result) => result.status === "partial"
24220
+ ).length,
24221
+ membershipInferredCount: results.filter(
24222
+ (result) => result.status === "inferred"
24223
+ ).length,
24224
+ errorCount: errors.length,
24225
+ rateLimit: { maxLists, memberLimit, maxMemberPages, delayMs },
24226
+ lists: results
24227
+ };
24228
+ });
24229
+ });
24230
+ }
24231
+ function syncXLists(options = {}) {
24232
+ return runEffectPromise(syncXListsEffect(options));
24233
+ }
24234
+ function accountIdForRead(db, account) {
24235
+ return resolveLiveSyncAccount(db, account).accountId;
24236
+ }
24237
+ function storedListFromRow(row) {
24238
+ return {
24239
+ accountId: String(row.account_id),
24240
+ listId: String(row.list_id),
24241
+ name: String(row.name),
24242
+ description: String(row.description ?? ""),
24243
+ ...row.owner_profile_id ? { ownerProfileId: String(row.owner_profile_id) } : {},
24244
+ ...row.owner_external_user_id ? { ownerExternalUserId: String(row.owner_external_user_id) } : {},
24245
+ isPrivate: Number(row.is_private) === 1,
24246
+ ...typeof row.member_count === "number" ? { memberCount: row.member_count } : {},
24247
+ ...typeof row.follower_count === "number" ? { followerCount: row.follower_count } : {},
24248
+ source: String(row.source),
24249
+ membershipStatus: String(row.membership_status),
24250
+ listsSyncedAt: String(row.lists_synced_at),
24251
+ ...row.members_synced_at ? { membersSyncedAt: String(row.members_synced_at) } : {},
24252
+ memberPageCount: Number(row.member_page_count),
24253
+ memberResultCount: Number(row.member_result_count),
24254
+ rateLimit: parseJsonRecord(row.rate_limit_json)
24255
+ };
24256
+ }
24257
+ function listStoredXLists(options = {}) {
24258
+ const db = getReadDb();
24259
+ const accountId = accountIdForRead(db, options.account);
24260
+ return db.prepare(
24261
+ `select * from x_lists where account_id = ? order by name collate nocase, list_id`
24262
+ ).all(accountId).map(storedListFromRow);
24263
+ }
24264
+ function resolveStoredXListSelector({
24265
+ account,
24266
+ listId,
24267
+ name
24268
+ }) {
24269
+ if (Boolean(listId) === Boolean(name)) {
24270
+ throw new Error("Choose exactly one of --list or --list-id");
24271
+ }
24272
+ const db = getReadDb();
24273
+ const accountId = accountIdForRead(db, account);
24274
+ const rows = listId ? db.prepare("select * from x_lists where account_id = ? and list_id = ?").all(accountId, listId) : db.prepare(
24275
+ "select * from x_lists where account_id = ? and name = ? collate nocase order by list_id"
24276
+ ).all(accountId, name);
24277
+ if (rows.length === 0) {
24278
+ throw new Error(
24279
+ `No cached List matches ${listId ? `id ${listId}` : `name ${name}`}; run birdclaw sync lists`
24280
+ );
24281
+ }
24282
+ if (rows.length > 1) {
24283
+ throw new Error(
24284
+ `List name ${name} is ambiguous; use --list-id (${rows.map((row) => String(row.list_id)).join(", ")})`
24285
+ );
24286
+ }
24287
+ return storedListFromRow(rows[0]);
24288
+ }
24289
+ function listStoredXListMembers({
24290
+ account,
24291
+ listId,
24292
+ name,
24293
+ includeEnded = false,
24294
+ limit = 100
24295
+ }) {
24296
+ const list = resolveStoredXListSelector({ account, listId, name });
24297
+ const db = getReadDb();
24298
+ const rows = db.prepare(
24299
+ `
24300
+ select
24301
+ member.account_id, member.list_id, member.external_user_id,
24302
+ member.current, member.first_seen_at, member.last_seen_at,
24303
+ member.ended_at, profile.id as profile_id, profile.handle,
24304
+ profile.display_name, profile.bio, profile.followers_count,
24305
+ profile.avatar_url
24306
+ from x_list_members member
24307
+ join profiles profile on profile.id = member.profile_id
24308
+ where member.account_id = ? and member.list_id = ?
24309
+ and (? = 1 or member.current = 1)
24310
+ order by profile.followers_count desc, profile.handle collate nocase
24311
+ limit ?
24312
+ `
24313
+ ).all(
24314
+ list.accountId,
24315
+ list.listId,
24316
+ includeEnded ? 1 : 0,
24317
+ positiveInteger("--limit", limit, 1e4)
24318
+ );
24319
+ const items = rows.map((row) => ({
24320
+ accountId: String(row.account_id),
24321
+ listId: String(row.list_id),
24322
+ externalUserId: String(row.external_user_id),
24323
+ current: Number(row.current) === 1,
24324
+ firstSeenAt: String(row.first_seen_at),
24325
+ lastSeenAt: String(row.last_seen_at),
24326
+ ...row.ended_at ? { endedAt: String(row.ended_at) } : {},
24327
+ profile: {
24328
+ id: String(row.profile_id),
24329
+ handle: String(row.handle),
24330
+ displayName: String(row.display_name),
24331
+ bio: String(row.bio),
24332
+ followersCount: Number(row.followers_count),
24333
+ ...row.avatar_url ? { avatarUrl: String(row.avatar_url) } : {}
24334
+ }
24335
+ }));
24336
+ return { list, items };
24337
+ }
24338
+
24339
+ // src/cli/register-lists.ts
24340
+ function registerListCommands({
24341
+ program: program2,
24342
+ print: print2,
24343
+ asJson,
24344
+ autoUpdateBeforeRead: autoUpdateBeforeRead2
24345
+ }) {
24346
+ const listsCommand = program2.command("lists").description("Inspect cached X Lists and membership");
24347
+ listsCommand.command("list").description("List cached owned X Lists").option("--account <accountId>", "Account id").action(async (options) => {
24348
+ await autoUpdateBeforeRead2();
24349
+ const items = listStoredXLists({ account: options.account });
24350
+ if (asJson()) {
24351
+ print2(items, true);
24352
+ return;
24353
+ }
24354
+ console.log(
24355
+ items.map(
24356
+ (item) => `${item.name} ${item.listId} ${item.membershipStatus} ${String(item.memberResultCount)}/${item.memberCount === void 0 ? "?" : String(item.memberCount)}`
24357
+ ).join("\n")
24358
+ );
24359
+ });
24360
+ listsCommand.command("members [name]").description("List cached members for one X List").option("--list-id <id>", "Select by List id").option("--account <accountId>", "Account id").option("--include-ended", "Include members absent from a complete sync").option("--limit <n>", "Limit results", "100").action(async (name, options) => {
24361
+ await autoUpdateBeforeRead2();
24362
+ const result = listStoredXListMembers({
24363
+ account: options.account,
24364
+ name,
24365
+ listId: options.listId,
24366
+ includeEnded: Boolean(options.includeEnded),
24367
+ limit: Number(options.limit)
24368
+ });
24369
+ if (asJson()) {
24370
+ print2(result, true);
24371
+ return;
24372
+ }
24373
+ console.log(
24374
+ result.items.map(
24375
+ (item) => `@${item.profile.handle} ${item.externalUserId} ${item.current ? "current" : "ended"}`
24376
+ ).join("\n")
24377
+ );
24378
+ });
24379
+ }
24380
+
23375
24381
  // src/lib/profile-replies.ts
23376
- import { Effect as Effect50 } from "effect";
24382
+ import { Effect as Effect51 } from "effect";
23377
24383
 
23378
24384
  // src/lib/moderation-target.ts
23379
- import { Effect as Effect49 } from "effect";
24385
+ import { Effect as Effect50 } from "effect";
23380
24386
 
23381
24387
  // src/lib/bird-actions.ts
23382
- import { Effect as Effect48 } from "effect";
24388
+ import { Effect as Effect49 } from "effect";
23383
24389
 
23384
24390
  // src/lib/bird-command.ts
23385
24391
  import { access } from "node:fs/promises";
23386
24392
  import { constants as constants2 } from "node:fs";
23387
- import { Data as Data3, Effect as Effect47 } from "effect";
24393
+ import { Data as Data3, Effect as Effect48 } from "effect";
23388
24394
  var BirdCommandUnavailableError = class extends Data3.TaggedError(
23389
24395
  "BirdCommandUnavailableError"
23390
24396
  ) {
@@ -23437,25 +24443,25 @@ function execFailureFromCause(command, cause) {
23437
24443
  }
23438
24444
  function assertBirdCommandAvailableEffect(command) {
23439
24445
  if (!isPathCommand(command)) {
23440
- return Effect47.void;
24446
+ return Effect48.void;
23441
24447
  }
23442
- return Effect47.tryPromise({
24448
+ return Effect48.tryPromise({
23443
24449
  try: () => Promise.resolve(access(command, constants2.X_OK)),
23444
24450
  catch: (cause) => new BirdCommandUnavailableError({
23445
24451
  message: formatBirdInstallHint(command),
23446
24452
  command,
23447
24453
  cause
23448
24454
  })
23449
- }).pipe(Effect47.asVoid);
24455
+ }).pipe(Effect48.asVoid);
23450
24456
  }
23451
24457
  function getBirdCommandEffect() {
23452
- return Effect47.try({
24458
+ return Effect48.try({
23453
24459
  try: () => getBirdCommand(),
23454
24460
  catch: (cause) => cause instanceof Error ? cause : new Error(String(cause))
23455
24461
  });
23456
24462
  }
23457
24463
  function runBirdCommandEffect(args, options) {
23458
- return Effect47.gen(function* () {
24464
+ return Effect48.gen(function* () {
23459
24465
  const birdCommand = yield* getBirdCommandEffect();
23460
24466
  yield* assertBirdCommandAvailableEffect(birdCommand);
23461
24467
  return yield* runSubprocessEffect({
@@ -23468,8 +24474,8 @@ function runBirdCommandEffect(args, options) {
23468
24474
  ...options?.signal ? { signal: options.signal } : {},
23469
24475
  ...options?.killSignal ? { killSignal: options.killSignal } : {}
23470
24476
  }).pipe(
23471
- Effect47.map(({ stdout, stderr }) => ({ stdout, stderr })),
23472
- Effect47.mapError((cause) => execFailureFromCause(birdCommand, cause))
24477
+ Effect48.map(({ stdout, stderr }) => ({ stdout, stderr })),
24478
+ Effect48.mapError((cause) => execFailureFromCause(birdCommand, cause))
23473
24479
  );
23474
24480
  });
23475
24481
  }
@@ -23507,9 +24513,9 @@ function toError18(error) {
23507
24513
  return error instanceof Error ? error : new Error(String(error));
23508
24514
  }
23509
24515
  function runBirdJsonCommandEffect2(args) {
23510
- return Effect48.gen(function* () {
24516
+ return Effect49.gen(function* () {
23511
24517
  const { stdout } = yield* runBirdCommandEffect(args);
23512
- return yield* Effect48.try({
24518
+ return yield* Effect49.try({
23513
24519
  try: () => JSON.parse(stripAnsi2(stdout)),
23514
24520
  catch: toError18
23515
24521
  });
@@ -23523,13 +24529,13 @@ function safeBirdProfileArg(query) {
23523
24529
  throw new Error("Invalid bird profile query");
23524
24530
  }
23525
24531
  function readBirdStatusViaBirdEffect(query) {
23526
- return Effect48.gen(function* () {
23527
- const safeQuery = yield* Effect48.try({
24532
+ return Effect49.gen(function* () {
24533
+ const safeQuery = yield* Effect49.try({
23528
24534
  try: () => safeBirdProfileArg(query),
23529
24535
  catch: toError18
23530
24536
  });
23531
24537
  return yield* runBirdJsonCommandEffect2(["status", safeQuery, "--json"]);
23532
- }).pipe(Effect48.catchAll(() => Effect48.succeed(null)));
24538
+ }).pipe(Effect49.catchAll(() => Effect49.succeed(null)));
23533
24539
  }
23534
24540
  function readBirdStatusViaBird(query) {
23535
24541
  return runEffectPromise(readBirdStatusViaBirdEffect(query));
@@ -23566,8 +24572,8 @@ function toBirdLookupUser(payload) {
23566
24572
  };
23567
24573
  }
23568
24574
  function lookupProfileViaBirdEffect2(query) {
23569
- return Effect48.gen(function* () {
23570
- const safeQuery = yield* Effect48.try({
24575
+ return Effect49.gen(function* () {
24576
+ const safeQuery = yield* Effect49.try({
23571
24577
  try: () => safeBirdProfileArg(query),
23572
24578
  catch: toError18
23573
24579
  });
@@ -23578,11 +24584,11 @@ function lookupProfileViaBirdEffect2(query) {
23578
24584
  "1",
23579
24585
  "--json"
23580
24586
  ]);
23581
- return yield* Effect48.try({
24587
+ return yield* Effect49.try({
23582
24588
  try: () => toBirdLookupUser(payload),
23583
24589
  catch: toError18
23584
24590
  });
23585
- }).pipe(Effect48.catchAll(() => Effect48.succeed(null)));
24591
+ }).pipe(Effect49.catchAll(() => Effect49.succeed(null)));
23586
24592
  }
23587
24593
  function lookupProfileViaBird(query) {
23588
24594
  return runEffectPromise(lookupProfileViaBirdEffect2(query));
@@ -23593,8 +24599,8 @@ function runVerifiedBirdMutationEffect({
23593
24599
  verifyField,
23594
24600
  expectedValue
23595
24601
  }) {
23596
- return Effect48.gen(function* () {
23597
- const safeQuery = yield* Effect48.try({
24602
+ return Effect49.gen(function* () {
24603
+ const safeQuery = yield* Effect49.try({
23598
24604
  try: () => safeBirdProfileArg(query),
23599
24605
  catch: toError18
23600
24606
  });
@@ -23608,12 +24614,12 @@ function runVerifiedBirdMutationEffect({
23608
24614
  action,
23609
24615
  safeQuery
23610
24616
  ]).pipe(
23611
- Effect48.map(({ stdout, stderr }) => ({
24617
+ Effect49.map(({ stdout, stderr }) => ({
23612
24618
  ok: true,
23613
24619
  output: normalizeOutput(stdout, stderr)
23614
24620
  })),
23615
- Effect48.catchAll(
23616
- (error) => Effect48.succeed({
24621
+ Effect49.catchAll(
24622
+ (error) => Effect49.succeed({
23617
24623
  ok: false,
23618
24624
  output: formatExecError2(error, `bird ${action} failed`)
23619
24625
  })
@@ -23692,7 +24698,7 @@ function toError19(error) {
23692
24698
  return error instanceof Error ? error : new Error(String(error));
23693
24699
  }
23694
24700
  function trySync14(try_) {
23695
- return Effect49.try({
24701
+ return Effect50.try({
23696
24702
  try: try_,
23697
24703
  catch: toError19
23698
24704
  });
@@ -23740,11 +24746,11 @@ function resolveLocalProfile(db, normalizedQuery) {
23740
24746
  };
23741
24747
  }
23742
24748
  function resolveProfileEffect(query, providedDb) {
23743
- return Effect49.gen(function* () {
24749
+ return Effect50.gen(function* () {
23744
24750
  const db = providedDb ?? (yield* trySync14(() => getNativeDb()));
23745
24751
  const normalizedQuery = normalizeProfileQuery(query);
23746
24752
  if (!normalizedQuery) {
23747
- return yield* Effect49.fail(new Error("Missing profile handle or id"));
24753
+ return yield* Effect50.fail(new Error("Missing profile handle or id"));
23748
24754
  }
23749
24755
  const local = yield* trySync14(
23750
24756
  () => resolveLocalProfile(db, normalizedQuery)
@@ -23753,7 +24759,7 @@ function resolveProfileEffect(query, providedDb) {
23753
24759
  if (local) {
23754
24760
  return local;
23755
24761
  }
23756
- return yield* Effect49.fail(
24762
+ return yield* Effect50.fail(
23757
24763
  new Error(`Profile not found locally: ${query}`)
23758
24764
  );
23759
24765
  }
@@ -23765,8 +24771,8 @@ function resolveProfileEffect(query, providedDb) {
23765
24771
  const birdResult = yield* tryPromise(
23766
24772
  () => lookupProfileViaBird(local?.profile.handle ?? normalizedQuery)
23767
24773
  ).pipe(
23768
- Effect49.map((value) => ({ ok: true, value })),
23769
- Effect49.catchAll((error) => Effect49.succeed({ ok: false, error }))
24774
+ Effect50.map((value) => ({ ok: true, value })),
24775
+ Effect50.catchAll((error) => Effect50.succeed({ ok: false, error }))
23770
24776
  );
23771
24777
  if (birdResult.ok) {
23772
24778
  user = birdResult.value ?? void 0;
@@ -23777,9 +24783,9 @@ function resolveProfileEffect(query, providedDb) {
23777
24783
  const xurlResult = yield* tryPromise(
23778
24784
  () => /^\d+$/.test(normalizedQuery) ? lookupUsersByIds([normalizedQuery]) : lookupUsersByHandles([local?.profile.handle ?? normalizedQuery])
23779
24785
  ).pipe(
23780
- Effect49.map((value) => ({ ok: true, value })),
23781
- Effect49.catchAll(
23782
- (error) => Effect49.succeed({ ok: false, error })
24786
+ Effect50.map((value) => ({ ok: true, value })),
24787
+ Effect50.catchAll(
24788
+ (error) => Effect50.succeed({ ok: false, error })
23783
24789
  )
23784
24790
  );
23785
24791
  if (xurlResult.ok) {
@@ -23792,13 +24798,13 @@ function resolveProfileEffect(query, providedDb) {
23792
24798
  if (local) {
23793
24799
  return local;
23794
24800
  }
23795
- return yield* Effect49.fail(lastError);
24801
+ return yield* Effect50.fail(lastError);
23796
24802
  }
23797
24803
  if (!user) {
23798
24804
  if (local) {
23799
24805
  return local;
23800
24806
  }
23801
- return yield* Effect49.fail(new Error(`Profile not found: ${query}`));
24807
+ return yield* Effect50.fail(new Error(`Profile not found: ${query}`));
23802
24808
  }
23803
24809
  if (local) {
23804
24810
  return yield* databaseWriteEffect(
@@ -23839,11 +24845,11 @@ function inspectProfileReplies(query, { limit = 12 } = {}) {
23839
24845
  return runEffectPromise(inspectProfileRepliesEffect(query, { limit }));
23840
24846
  }
23841
24847
  function inspectProfileRepliesEffect(query, { limit = 12 } = {}) {
23842
- return Effect50.gen(function* () {
24848
+ return Effect51.gen(function* () {
23843
24849
  const resolved = yield* tryPromise(() => resolveProfile(query));
23844
24850
  const externalUserId = resolved.externalUserId;
23845
24851
  if (!externalUserId) {
23846
- return yield* Effect50.fail(
24852
+ return yield* Effect51.fail(
23847
24853
  new Error(`Profile has no external Twitter user id: ${query}`)
23848
24854
  );
23849
24855
  }
@@ -23938,18 +24944,18 @@ function registerMentionCommands({
23938
24944
  }
23939
24945
 
23940
24946
  // src/lib/blocks.ts
23941
- import { Effect as Effect53 } from "effect";
24947
+ import { Effect as Effect54 } from "effect";
23942
24948
 
23943
24949
  // src/lib/moderation-state.ts
23944
- import { Effect as Effect52 } from "effect";
24950
+ import { Effect as Effect53 } from "effect";
23945
24951
 
23946
24952
  // src/lib/actions-transport.ts
23947
- import { Effect as Effect51 } from "effect";
24953
+ import { Effect as Effect52 } from "effect";
23948
24954
  function toError20(error) {
23949
24955
  return error instanceof Error ? error : new Error(String(error));
23950
24956
  }
23951
24957
  function trySync15(try_) {
23952
- return Effect51.try({
24958
+ return Effect52.try({
23953
24959
  try: try_,
23954
24960
  catch: toError20
23955
24961
  });
@@ -23961,7 +24967,7 @@ function liveWritesDisabled4() {
23961
24967
  return process.env.BIRDCLAW_DISABLE_LIVE_WRITES === "1";
23962
24968
  }
23963
24969
  function verifyExpectedAccountEffect(expectedAccount) {
23964
- return Effect51.gen(function* () {
24970
+ return Effect52.gen(function* () {
23965
24971
  if (!expectedAccount) return null;
23966
24972
  if (liveWritesDisabled4()) return null;
23967
24973
  const sourceUser = yield* tryPromise(() => lookupAuthenticatedUserFresh());
@@ -23971,7 +24977,7 @@ function verifyExpectedAccountEffect(expectedAccount) {
23971
24977
  const expectedHandle = profileHandleKey(expectedAccount.handle);
23972
24978
  if (expectedExternalUserId) {
23973
24979
  if (sourceUserId === expectedExternalUserId) return sourceUserId;
23974
- return yield* Effect51.fail(
24980
+ return yield* Effect52.fail(
23975
24981
  new Error(
23976
24982
  `xurl is authenticated as user ${sourceUserId || "unknown"}, not account ${expectedAccount.id}`
23977
24983
  )
@@ -23979,7 +24985,7 @@ function verifyExpectedAccountEffect(expectedAccount) {
23979
24985
  }
23980
24986
  if (expectedHandle && sourceUsername === expectedHandle)
23981
24987
  return sourceUserId;
23982
- return yield* Effect51.fail(
24988
+ return yield* Effect52.fail(
23983
24989
  new Error(
23984
24990
  sourceUsername ? `xurl is authenticated as @${sourceUsername}, not @${expectedHandle}` : "xurl authenticated user unavailable"
23985
24991
  )
@@ -23987,7 +24993,7 @@ function verifyExpectedAccountEffect(expectedAccount) {
23987
24993
  });
23988
24994
  }
23989
24995
  function runBirdActionEffect(action, query) {
23990
- return Effect51.gen(function* () {
24996
+ return Effect52.gen(function* () {
23991
24997
  const result = yield* tryPromise(
23992
24998
  () => action === "block" ? blockUserViaBird(query) : action === "unblock" ? unblockUserViaBird(query) : action === "mute" ? muteUserViaBird(query) : unmuteUserViaBird(query)
23993
24999
  );
@@ -24007,7 +25013,7 @@ function getVerifyExpectation(action) {
24007
25013
  };
24008
25014
  }
24009
25015
  function runXurlActionEffect(action, query, targetUserId, verifiedSourceUserId) {
24010
- return Effect51.gen(function* () {
25016
+ return Effect52.gen(function* () {
24011
25017
  if (!targetUserId) {
24012
25018
  return {
24013
25019
  ok: false,
@@ -24072,13 +25078,13 @@ function runModerationActionEffect({
24072
25078
  transport,
24073
25079
  expectedAccount
24074
25080
  }) {
24075
- return Effect51.gen(function* () {
25081
+ return Effect52.gen(function* () {
24076
25082
  const requestedTransport = yield* trySync15(
24077
25083
  () => resolveActionsTransport(transport)
24078
25084
  );
24079
25085
  const verifyXurlAccount = () => verifyExpectedAccountEffect(expectedAccount).pipe(
24080
- Effect51.catchAll(
24081
- (error) => Effect51.succeed({
25086
+ Effect52.catchAll(
25087
+ (error) => Effect52.succeed({
24082
25088
  ok: false,
24083
25089
  output: error instanceof Error ? error.message : String(error),
24084
25090
  transport: "xurl"
@@ -24167,7 +25173,7 @@ function toError21(error) {
24167
25173
  return error instanceof Error ? error : new Error(String(error));
24168
25174
  }
24169
25175
  function trySync16(try_) {
24170
- return Effect52.try({
25176
+ return Effect53.try({
24171
25177
  try: try_,
24172
25178
  catch: toError21
24173
25179
  });
@@ -24286,20 +25292,20 @@ function resolveModerationTargetEffect({
24286
25292
  query,
24287
25293
  selfActionError
24288
25294
  }) {
24289
- return Effect52.gen(function* () {
25295
+ return Effect53.gen(function* () {
24290
25296
  const db = yield* trySync16(() => getNativeDb());
24291
25297
  const resolvedAccountId = accountId || getDefaultAccountId(db);
24292
25298
  const accountHandle = yield* trySync16(
24293
25299
  () => getAccountHandle(db, resolvedAccountId)
24294
25300
  );
24295
25301
  if (!accountHandle) {
24296
- return yield* Effect52.fail(
25302
+ return yield* Effect53.fail(
24297
25303
  new Error(`Unknown account: ${resolvedAccountId}`)
24298
25304
  );
24299
25305
  }
24300
25306
  const normalizedQuery = normalizeProfileQuery(query);
24301
25307
  if (normalizedQuery.toLowerCase() === accountHandle.toLowerCase()) {
24302
- return yield* Effect52.fail(new Error(selfActionError));
25308
+ return yield* Effect53.fail(new Error(selfActionError));
24303
25309
  }
24304
25310
  const resolved = yield* resolveProfileEffect(query, db);
24305
25311
  const account = yield* trySync16(
@@ -24310,7 +25316,7 @@ function resolveModerationTargetEffect({
24310
25316
  );
24311
25317
  const accountExternalUserId = account?.external_user_id ?? getExternalUserId(accountProfile?.id ?? "");
24312
25318
  if (profileHandleKey(resolved.profile.handle) === profileHandleKey(accountHandle) || accountProfile?.id && resolved.profile.id === accountProfile.id || accountExternalUserId && resolved.externalUserId === accountExternalUserId) {
24313
- return yield* Effect52.fail(new Error(selfActionError));
25319
+ return yield* Effect53.fail(new Error(selfActionError));
24314
25320
  }
24315
25321
  return {
24316
25322
  db,
@@ -24327,7 +25333,7 @@ function resolveModerationTargetEffect({
24327
25333
  }
24328
25334
  function addModerationEffect(kind, accountId, query, options = {}) {
24329
25335
  const descriptor = moderationDescriptors[kind];
24330
- return Effect52.gen(function* () {
25336
+ return Effect53.gen(function* () {
24331
25337
  const { db, resolved, resolvedAccountId, accountIdentity, actionQuery } = yield* resolveModerationTargetEffect({
24332
25338
  accountId,
24333
25339
  query,
@@ -24377,7 +25383,7 @@ function addModeration(kind, accountId, query, options = {}) {
24377
25383
  }
24378
25384
  function recordModerationEffect(kind, accountId, query) {
24379
25385
  const descriptor = moderationDescriptors[kind];
24380
- return Effect52.gen(function* () {
25386
+ return Effect53.gen(function* () {
24381
25387
  const { db, resolved, resolvedAccountId } = yield* resolveModerationTargetEffect({
24382
25388
  accountId,
24383
25389
  query,
@@ -24408,7 +25414,7 @@ function recordModeration(kind, accountId, query) {
24408
25414
  }
24409
25415
  function removeModerationEffect(kind, accountId, query, options = {}) {
24410
25416
  const descriptor = moderationDescriptors[kind];
24411
- return Effect52.gen(function* () {
25417
+ return Effect53.gen(function* () {
24412
25418
  const { db, resolved, resolvedAccountId, accountIdentity, actionQuery } = yield* resolveModerationTargetEffect({
24413
25419
  accountId,
24414
25420
  query,
@@ -24484,7 +25490,7 @@ function toError22(error) {
24484
25490
  return error instanceof Error ? error : new Error(String(error));
24485
25491
  }
24486
25492
  function trySync17(try_) {
24487
- return Effect53.try({
25493
+ return Effect54.try({
24488
25494
  try: try_,
24489
25495
  catch: toError22
24490
25496
  });
@@ -24497,12 +25503,12 @@ function listBlocks({
24497
25503
  return listModerationState("block", { account, search, limit });
24498
25504
  }
24499
25505
  function syncBlocksEffect(accountId) {
24500
- return Effect53.gen(function* () {
25506
+ return Effect54.gen(function* () {
24501
25507
  const db = yield* trySync17(() => getNativeDb());
24502
25508
  const resolvedAccountId = accountId || getDefaultAccountId(db);
24503
25509
  const accountHandle = getAccountHandle(db, resolvedAccountId);
24504
25510
  if (!accountHandle) {
24505
- return yield* Effect53.fail(
25511
+ return yield* Effect54.fail(
24506
25512
  new Error(`Unknown account: ${resolvedAccountId}`)
24507
25513
  );
24508
25514
  }
@@ -24523,9 +25529,9 @@ function syncBlocksEffect(accountId) {
24523
25529
  }
24524
25530
  };
24525
25531
  }
24526
- return yield* Effect53.gen(function* () {
25532
+ return yield* Effect54.gen(function* () {
24527
25533
  const me = yield* tryPromise(() => lookupAuthenticatedUserFresh()).pipe(
24528
- Effect53.mapError(toError22)
25534
+ Effect54.mapError(toError22)
24529
25535
  );
24530
25536
  const sourceUserId = typeof me?.id === "string" && me.id.length > 0 ? me.id : null;
24531
25537
  const sourceUsername = typeof me?.username === "string" ? me.username.replace(/^@/, "") : "";
@@ -24572,7 +25578,7 @@ function syncBlocksEffect(accountId) {
24572
25578
  do {
24573
25579
  const page = yield* tryPromise(
24574
25580
  () => listBlockedUsers(sourceUserId, nextToken ?? void 0)
24575
- ).pipe(Effect53.mapError(toError22));
25581
+ ).pipe(Effect54.mapError(toError22));
24576
25582
  const pageProfileIds = yield* databaseWriteEffect((writeDb) => {
24577
25583
  return page.items.map((user) => {
24578
25584
  const resolved = upsertProfileFromXUser(writeDb, user);
@@ -24614,8 +25620,8 @@ function syncBlocksEffect(accountId) {
24614
25620
  }
24615
25621
  };
24616
25622
  }).pipe(
24617
- Effect53.catchAll(
24618
- (error) => Effect53.succeed({
25623
+ Effect54.catchAll(
25624
+ (error) => Effect54.succeed({
24619
25625
  ok: false,
24620
25626
  accountId: resolvedAccountId,
24621
25627
  synced: remoteProfileIds.length > 0,
@@ -24766,7 +25772,7 @@ function registerModerationCommands({
24766
25772
 
24767
25773
  // src/lib/blocklist.ts
24768
25774
  import { readFile } from "node:fs/promises";
24769
- import { Effect as Effect54 } from "effect";
25775
+ import { Effect as Effect55 } from "effect";
24770
25776
  function stripInlineCode(value) {
24771
25777
  const match = value.match(/`([^`]+)`/);
24772
25778
  return match?.[1] ?? value;
@@ -24803,15 +25809,15 @@ function parseBlocklistText(text) {
24803
25809
  return items;
24804
25810
  }
24805
25811
  function importBlocklistEffect(accountId, filePath) {
24806
- return Effect54.gen(function* () {
25812
+ return Effect55.gen(function* () {
24807
25813
  const text = yield* tryPromise(() => readFile(filePath, "utf8"));
24808
25814
  const queries = parseBlocklistText(text);
24809
25815
  const items = [];
24810
25816
  for (const query of queries) {
24811
25817
  const blocked = yield* tryPromise(() => addBlock(accountId, query)).pipe(
24812
- Effect54.map((result2) => ({ ok: true, result: result2 })),
24813
- Effect54.catchAll(
24814
- (error) => Effect54.succeed({ ok: false, error })
25818
+ Effect55.map((result2) => ({ ok: true, result: result2 })),
25819
+ Effect55.catchAll(
25820
+ (error) => Effect55.succeed({ ok: false, error })
24815
25821
  )
24816
25822
  );
24817
25823
  if (!blocked.ok) {
@@ -24867,7 +25873,7 @@ function registerModerationCommands2(context) {
24867
25873
  }
24868
25874
 
24869
25875
  // src/lib/link-index.ts
24870
- import { Effect as Effect55 } from "effect";
25876
+ import { Effect as Effect56 } from "effect";
24871
25877
  var DEFAULT_EXPAND_CONCURRENCY = 12;
24872
25878
  function parseJsonField5(value, fallback) {
24873
25879
  if (typeof value !== "string" || value.length === 0) {
@@ -25029,7 +26035,7 @@ function expandWithConcurrencyEffect(db, urls, options) {
25029
26035
  misses: 0,
25030
26036
  errors: 0
25031
26037
  };
25032
- return Effect55.forEach(
26038
+ return Effect56.forEach(
25033
26039
  urls,
25034
26040
  (url) => tryPromise(
25035
26041
  () => expandUrls([url], {
@@ -25038,7 +26044,7 @@ function expandWithConcurrencyEffect(db, urls, options) {
25038
26044
  timeoutMs: options.timeoutMs
25039
26045
  })
25040
26046
  ).pipe(
25041
- Effect55.map((results) => {
26047
+ Effect56.map((results) => {
25042
26048
  const result = results[0];
25043
26049
  if (result.source === "network") {
25044
26050
  counts.networkExpansions++;
@@ -25055,10 +26061,10 @@ function expandWithConcurrencyEffect(db, urls, options) {
25055
26061
  })
25056
26062
  ),
25057
26063
  { concurrency }
25058
- ).pipe(Effect55.as(counts));
26064
+ ).pipe(Effect56.as(counts));
25059
26065
  }
25060
26066
  function backfillLinkIndexEffect(options = {}) {
25061
- return Effect55.gen(function* () {
26067
+ return Effect56.gen(function* () {
25062
26068
  const db = getNativeDb({ seedDemoData: false });
25063
26069
  const { occurrences, entityExpansions } = rebuildOccurrences(
25064
26070
  db,
@@ -25353,7 +26359,7 @@ import {
25353
26359
  } from "node:fs";
25354
26360
  import { appendFile, copyFile, rename, rm, writeFile } from "node:fs/promises";
25355
26361
  import path11 from "node:path";
25356
- import { Effect as Effect56 } from "effect";
26362
+ import { Effect as Effect57 } from "effect";
25357
26363
  var DEFAULT_MAX_BYTES = 100 * 1024 * 1024;
25358
26364
  var PBS_PREFIXES = [
25359
26365
  "/media/",
@@ -25372,13 +26378,13 @@ function toMediaError(error) {
25372
26378
  return error instanceof Error ? error : new Error(String(error));
25373
26379
  }
25374
26380
  function tryMediaSync(try_) {
25375
- return Effect56.try({
26381
+ return Effect57.try({
25376
26382
  try: try_,
25377
26383
  catch: toMediaError
25378
26384
  });
25379
26385
  }
25380
26386
  function tryMediaPromise(try_) {
25381
- return Effect56.tryPromise({
26387
+ return Effect57.tryPromise({
25382
26388
  try: try_,
25383
26389
  catch: toMediaError
25384
26390
  });
@@ -25643,7 +26649,7 @@ function archivePathFor(item, mediaOriginalsDir) {
25643
26649
  return archiveTweetDirs(mediaOriginalsDir, item.tweetId).map((tweetDir) => path11.join(tweetDir, fileName)).find((archivePath) => existsSync5(archivePath)) ?? null;
25644
26650
  }
25645
26651
  function reuseFromArchiveEffect(item, mediaOriginalsDir, maxBytes) {
25646
- return Effect56.gen(function* () {
26652
+ return Effect57.gen(function* () {
25647
26653
  const archivePath = archivePathFor(item, mediaOriginalsDir);
25648
26654
  if (!archivePath || !existsSync5(archivePath)) return null;
25649
26655
  const bytes = fileSize(archivePath);
@@ -25670,15 +26676,15 @@ function contentRangeTotal(response) {
25670
26676
  return total ? Number(total) : null;
25671
26677
  }
25672
26678
  function writeResponseBodyEffect(response, tmpPath, append, maxBytes, initialBytes) {
25673
- return Effect56.gen(function* () {
26679
+ return Effect57.gen(function* () {
25674
26680
  if (!response.body)
25675
- return yield* Effect56.fail(new Error("missing response body"));
26681
+ return yield* Effect57.fail(new Error("missing response body"));
25676
26682
  let bytes = 0;
25677
26683
  if (!append) {
25678
26684
  yield* tryMediaPromise(() => writeFile(tmpPath, Buffer.alloc(0)));
25679
26685
  }
25680
26686
  const reader = response.body.getReader();
25681
- const result = yield* Effect56.gen(function* () {
26687
+ const result = yield* Effect57.gen(function* () {
25682
26688
  for (; ; ) {
25683
26689
  const { done, value } = yield* tryMediaPromise(() => reader.read());
25684
26690
  if (done) {
@@ -25688,31 +26694,31 @@ function writeResponseBodyEffect(response, tmpPath, append, maxBytes, initialByt
25688
26694
  bytes += chunk.length;
25689
26695
  if (initialBytes + bytes > maxBytes) {
25690
26696
  yield* tryMediaPromise(() => rm(tmpPath, { force: true })).pipe(
25691
- Effect56.catchAll(() => Effect56.void)
26697
+ Effect57.catchAll(() => Effect57.void)
25692
26698
  );
25693
- return yield* Effect56.fail(new Error("max-bytes"));
26699
+ return yield* Effect57.fail(new Error("max-bytes"));
25694
26700
  }
25695
26701
  yield* tryMediaPromise(() => appendFile(tmpPath, chunk));
25696
26702
  }
25697
26703
  return bytes;
25698
26704
  }).pipe(
25699
- Effect56.map((writtenBytes) => ({ ok: true, writtenBytes })),
25700
- Effect56.catchAll(
25701
- (error) => Effect56.gen(function* () {
26705
+ Effect57.map((writtenBytes) => ({ ok: true, writtenBytes })),
26706
+ Effect57.catchAll(
26707
+ (error) => Effect57.gen(function* () {
25702
26708
  if (error.message === "max-bytes") {
25703
26709
  yield* tryMediaPromise(() => rm(tmpPath, { force: true })).pipe(
25704
- Effect56.catchAll(() => Effect56.void)
26710
+ Effect57.catchAll(() => Effect57.void)
25705
26711
  );
25706
26712
  }
25707
26713
  yield* tryMediaPromise(() => reader.cancel(error)).pipe(
25708
- Effect56.catchAll(() => Effect56.void)
26714
+ Effect57.catchAll(() => Effect57.void)
25709
26715
  );
25710
26716
  return { error, ok: false };
25711
26717
  })
25712
26718
  ),
25713
- Effect56.ensuring(Effect56.sync(() => reader.releaseLock()))
26719
+ Effect57.ensuring(Effect57.sync(() => reader.releaseLock()))
25714
26720
  );
25715
- if (!result.ok) return yield* Effect56.fail(result.error);
26721
+ if (!result.ok) return yield* Effect57.fail(result.error);
25716
26722
  return result.writtenBytes;
25717
26723
  });
25718
26724
  }
@@ -25724,13 +26730,13 @@ function fetchOneEffect({
25724
26730
  userAgent,
25725
26731
  maxBytes
25726
26732
  }) {
25727
- return Effect56.gen(function* () {
26733
+ return Effect57.gen(function* () {
25728
26734
  let rateLimited = false;
25729
26735
  for (let attempt = 0; attempt <= retryMax; attempt += 1) {
25730
26736
  const partialBytes = item.kind === "image" ? 0 : fileSize(item.tmpPath);
25731
26737
  if (partialBytes > maxBytes) {
25732
26738
  yield* tryMediaPromise(() => rm(item.tmpPath, { force: true })).pipe(
25733
- Effect56.catchAll(() => Effect56.void)
26739
+ Effect57.catchAll(() => Effect57.void)
25734
26740
  );
25735
26741
  return fail(item, "max-bytes");
25736
26742
  }
@@ -25742,9 +26748,9 @@ function fetchOneEffect({
25742
26748
  }
25743
26749
  })
25744
26750
  ).pipe(
25745
- Effect56.map((response2) => ({ ok: true, response: response2 })),
25746
- Effect56.catchAll(
25747
- (error) => Effect56.succeed({ error, ok: false })
26751
+ Effect57.map((response2) => ({ ok: true, response: response2 })),
26752
+ Effect57.catchAll(
26753
+ (error) => Effect57.succeed({ error, ok: false })
25748
26754
  )
25749
26755
  );
25750
26756
  if (!responseResult.ok) {
@@ -25755,7 +26761,7 @@ function fetchOneEffect({
25755
26761
  rateLimited = true;
25756
26762
  if (attempt < retryMax) {
25757
26763
  yield* tryMediaPromise(() => sleep(1e3 * 2 ** attempt)).pipe(
25758
- Effect56.catchAll(() => Effect56.void)
26764
+ Effect57.catchAll(() => Effect57.void)
25759
26765
  );
25760
26766
  continue;
25761
26767
  }
@@ -25767,7 +26773,7 @@ function fetchOneEffect({
25767
26773
  const expectedTotal = contentRangeTotal(response) ?? (contentLength(response) ?? 0) + (response.status === 206 ? partialBytes : 0);
25768
26774
  if (expectedTotal > maxBytes) {
25769
26775
  yield* tryMediaPromise(() => rm(item.tmpPath, { force: true })).pipe(
25770
- Effect56.catchAll(() => Effect56.void)
26776
+ Effect57.catchAll(() => Effect57.void)
25771
26777
  );
25772
26778
  return fail(item, "max-bytes");
25773
26779
  }
@@ -25779,9 +26785,9 @@ function fetchOneEffect({
25779
26785
  maxBytes,
25780
26786
  append ? partialBytes : 0
25781
26787
  ).pipe(
25782
- Effect56.map((bytes) => ({ bytes, ok: true })),
25783
- Effect56.catchAll(
25784
- (error) => Effect56.succeed({ error, ok: false })
26788
+ Effect57.map((bytes) => ({ bytes, ok: true })),
26789
+ Effect57.catchAll(
26790
+ (error) => Effect57.succeed({ error, ok: false })
25785
26791
  )
25786
26792
  );
25787
26793
  if (!bytesResult.ok) {
@@ -25855,7 +26861,7 @@ function runGroupEffect(items, parallel, pacingMs, now2, sleep, worker) {
25855
26861
  );
25856
26862
  })
25857
26863
  );
25858
- return Effect56.forEach(pacedItems, runPaced, {
26864
+ return Effect57.forEach(pacedItems, runPaced, {
25859
26865
  concurrency: Math.min(parallel, items.length),
25860
26866
  discard: true
25861
26867
  });
@@ -25864,7 +26870,7 @@ function fetchTweetMedia(options = {}) {
25864
26870
  return runEffectPromise(fetchTweetMediaEffect(options));
25865
26871
  }
25866
26872
  function fetchTweetMediaEffect(options = {}) {
25867
- return Effect56.gen(function* () {
26873
+ return Effect57.gen(function* () {
25868
26874
  const now2 = options.now ?? Date.now;
25869
26875
  const startedAt = now2();
25870
26876
  const sleep = options.sleep ?? defaultSleep;
@@ -25931,7 +26937,7 @@ function fetchTweetMediaEffect(options = {}) {
25931
26937
  retryMax,
25932
26938
  userAgent,
25933
26939
  maxBytes
25934
- }).pipe(Effect56.map((fetched) => applyFetched(result, fetched)))
26940
+ }).pipe(Effect57.map((fetched) => applyFetched(result, fetched)))
25935
26941
  );
25936
26942
  yield* runGroupEffect(
25937
26943
  httpCandidates.filter((item) => item.kind === "image"),
@@ -25979,9 +26985,9 @@ function formatMediaFetchResult(result) {
25979
26985
  }
25980
26986
 
25981
26987
  // src/lib/whois.ts
25982
- import { Effect as Effect57 } from "effect";
26988
+ import { Effect as Effect58 } from "effect";
25983
26989
  function trySync18(try_) {
25984
- return Effect57.try({
26990
+ return Effect58.try({
25985
26991
  try: try_,
25986
26992
  catch: (cause) => cause
25987
26993
  });
@@ -26512,7 +27518,7 @@ function loadWhoisConversations(query, options, includeDms, context, limit) {
26512
27518
  return [...merged.values()];
26513
27519
  }
26514
27520
  function runWhoisEffect(query, options = {}) {
26515
- return Effect57.gen(function* () {
27521
+ return Effect58.gen(function* () {
26516
27522
  const includeDms = options.dms ?? true;
26517
27523
  const includeTweets = options.tweets ?? false;
26518
27524
  const limit = options.limit ?? 10;
@@ -26759,7 +27765,7 @@ function registerSearchCommands({
26759
27765
  parsePositiveIntegerOption: parsePositiveIntegerOption2
26760
27766
  }) {
26761
27767
  const searchCommand = program2.command("search").description("Search local data");
26762
- searchCommand.command("tweets [query]").option("--resource <resource>", "home, mentions, or authored", "home").option("--replied", "Only replied items").option("--unreplied", "Only unreplied items").option("--since <date>", "Include tweets created at or after this date").option("--until <date>", "Include tweets created before this date").option("--originals-only", "Exclude authored replies that start with @").option("--hide-low-quality", "Hide RTs, tiny replies, and link-only noise").option(
27768
+ searchCommand.command("tweets [query]").option("--resource <resource>", "home, mentions, or authored", "home").option("--account <accountId>", "Account id").option("--list <name>", "Only authors in a cached X List").option("--list-id <id>", "Only authors in a cached X List id").option("--replied", "Only replied items").option("--unreplied", "Only unreplied items").option("--since <date>", "Include tweets created at or after this date").option("--until <date>", "Include tweets created before this date").option("--originals-only", "Exclude authored replies that start with @").option("--hide-low-quality", "Hide RTs, tiny replies, and link-only noise").option(
26763
27769
  "--min-likes <n>",
26764
27770
  "Override the low-quality like threshold (default 50)"
26765
27771
  ).option("--quality-reason", "Include qualityReason on each row").option("--liked", "Only liked tweets").option("--bookmarked", "Only bookmarked tweets").option("--limit <n>", "Limit results", "20").action(async (query, options) => {
@@ -26769,10 +27775,18 @@ function registerSearchCommands({
26769
27775
  );
26770
27776
  if (options.minLikes !== void 0 && minLikes === void 0) return;
26771
27777
  await autoUpdateBeforeRead2();
27778
+ const selectedList = options.list || options.listId ? resolveStoredXListSelector({
27779
+ account: options.account,
27780
+ name: options.list,
27781
+ listId: options.listId
27782
+ }) : void 0;
26772
27783
  const replyFilter = options.replied ? "replied" : options.unreplied ? "unreplied" : "all";
26773
27784
  print2(
26774
27785
  listTimelineItems({
26775
27786
  resource: options.resource === "mentions" ? "mentions" : options.resource === "authored" ? "authored" : "home",
27787
+ account: options.account,
27788
+ listAccountId: selectedList?.accountId,
27789
+ listId: selectedList?.listId,
26776
27790
  search: query,
26777
27791
  replyFilter,
26778
27792
  since: options.since,
@@ -26955,7 +27969,7 @@ import { Readable as Readable2 } from "node:stream";
26955
27969
  import { pathToFileURL } from "node:url";
26956
27970
 
26957
27971
  // src/lib/http-effect.ts
26958
- import { Effect as Effect58 } from "effect";
27972
+ import { Effect as Effect59 } from "effect";
26959
27973
  var LOCAL_WEB_PEER_HEADER = "x-birdclaw-local-peer";
26960
27974
 
26961
27975
  // src/lib/production-server.ts
@@ -27229,7 +28243,7 @@ function registerStorageCommands({
27229
28243
  }
27230
28244
 
27231
28245
  // src/lib/authored-live.ts
27232
- import { Effect as Effect59 } from "effect";
28246
+ import { Effect as Effect60 } from "effect";
27233
28247
  var AuthoredSyncError = class extends Error {
27234
28248
  constructor(message, exitCode) {
27235
28249
  super(message);
@@ -27471,10 +28485,10 @@ function resolveAuthoredIdentityEffect({
27471
28485
  account,
27472
28486
  db
27473
28487
  }) {
27474
- return Effect59.gen(function* () {
28488
+ return Effect60.gen(function* () {
27475
28489
  const status = yield* liveTransportGateway.xurl.getTransportStatus();
27476
28490
  if (status.availableTransport !== "xurl") {
27477
- return yield* Effect59.fail(new AuthoredSyncError(status.statusText, 4));
28491
+ return yield* Effect60.fail(new AuthoredSyncError(status.statusText, 4));
27478
28492
  }
27479
28493
  const resolvedAccount = yield* trySync(
27480
28494
  () => resolveLiveSyncAccount(db, account)
@@ -27489,7 +28503,7 @@ function resolveAuthoredIdentityEffect({
27489
28503
  const authenticated = yield* liveTransportGateway.xurl.lookupAuthenticatedUser();
27490
28504
  const authenticatedUser = userFromAuthenticatedPayload(authenticated);
27491
28505
  if (!authenticatedUser?.id) {
27492
- return yield* Effect59.fail(
28506
+ return yield* Effect60.fail(
27493
28507
  new AuthoredSyncError(
27494
28508
  "Could not resolve authenticated Twitter user id",
27495
28509
  4
@@ -27497,7 +28511,7 @@ function resolveAuthoredIdentityEffect({
27497
28511
  );
27498
28512
  }
27499
28513
  if (normalizeUsername(authenticatedUser.username) !== normalizeUsername(resolvedAccount.username)) {
27500
- return yield* Effect59.fail(
28514
+ return yield* Effect60.fail(
27501
28515
  new AuthoredSyncError(
27502
28516
  `xurl is authenticated as @${authenticatedUser.username}, but selected account ${resolvedAccount.accountId} is @${resolvedAccount.username}. Link the account external_user_id or switch xurl login before syncing authored tweets.`,
27503
28517
  4
@@ -27796,9 +28810,9 @@ function syncAuthoredTweetsEffect({
27796
28810
  sinceId,
27797
28811
  untilId
27798
28812
  }) {
27799
- return Effect59.gen(function* () {
28813
+ return Effect60.gen(function* () {
27800
28814
  if (mode !== "xurl") {
27801
- return yield* Effect59.fail(
28815
+ return yield* Effect60.fail(
27802
28816
  new Error("authored sync only supports --mode xurl")
27803
28817
  );
27804
28818
  }
@@ -27861,7 +28875,7 @@ function syncAuthoredTweetsEffect({
27861
28875
  sourceUser
27862
28876
  })
27863
28877
  ).pipe(
27864
- Effect59.flatMap(
28878
+ Effect60.flatMap(
27865
28879
  () => trySync(() => {
27866
28880
  newestSeenId = maxTweetId(
27867
28881
  newestSeenId,
@@ -27964,6 +28978,32 @@ function registerSyncCommands({
27964
28978
  autoSyncAfterWrite: autoSyncAfterWrite2
27965
28979
  }) {
27966
28980
  const syncCommand = program2.command("sync").description("Refresh live Twitter collections into the local store");
28981
+ syncCommand.command("lists").description("Sync owned X Lists and rate-limited membership pages").option("--account <accountId>", "Account id").option("--mode <mode>", "auto, bird, or xurl", "auto").option("--max-lists <n>", "Maximum owned Lists to inspect", "20").option("--member-limit <n>", "Members per transport page", "20").option("--max-member-pages <n>", "Maximum membership pages per List", "1").option("--delay-ms <n>", "Delay between Lists", "1000").action(async (options) => {
28982
+ try {
28983
+ const result = await syncXLists({
28984
+ account: options.account,
28985
+ mode: options.mode,
28986
+ maxLists: Number(options.maxLists),
28987
+ memberLimit: Number(options.memberLimit),
28988
+ maxMemberPages: Number(options.maxMemberPages),
28989
+ delayMs: Number(options.delayMs)
28990
+ });
28991
+ await autoSyncAfterWrite2();
28992
+ print2(result, true);
28993
+ if (!result.ok) process.exitCode = 5;
28994
+ } catch (error) {
28995
+ print2(
28996
+ {
28997
+ ok: false,
28998
+ kind: "lists",
28999
+ mode: options.mode,
29000
+ error: errorMessage(error)
29001
+ },
29002
+ true
29003
+ );
29004
+ process.exitCode = 1;
29005
+ }
29006
+ });
27967
29007
  syncCommand.command("timeline").description("Refresh live home timeline through xurl or bird").option("--account <accountId>", "Account id").option("--mode <mode>", "auto, xurl, or bird", "auto").option("--limit <n>", "Result limit", "100").option("--max-pages <n>", "Stop after N xurl pages", "3").option("--for-you", 'Fetch "For You" instead of chronological Following').option("--cache-ttl <seconds>", "Live-cache freshness window", "120").option("--refresh", "Bypass live-cache freshness window").action(async (options) => {
27968
29008
  const result = await syncHomeTimeline({
27969
29009
  account: options.account,
@@ -28138,6 +29178,7 @@ registerMentionCommands(commandContext);
28138
29178
  registerDirectMessageCommands(commandContext);
28139
29179
  registerSyncCommands(commandContext);
28140
29180
  registerJobCommands(commandContext);
29181
+ registerListCommands(commandContext);
28141
29182
  registerModerationCommands2(commandContext);
28142
29183
  registerComposeCommands(commandContext);
28143
29184
  registerInboxCommand(commandContext);