birdclaw 0.8.5 → 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 +24 -0
  2. package/README.md +22 -2
  3. package/dist/cli/birdclaw.js +1228 -186
  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-LbYOsoKf.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-DYNcLcll.js → mapbox-gl-WUhsQcSS.js} +1 -1
  23. package/dist/client/assets/{mentions-BoVVAIT7.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-CiLuYpZr.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-L7bs6IHH.js → ProfileAnalysisStream-CLjrYzEx.js} +2 -2
  46. package/dist/server/assets/{ProfilePreview-Cg-AOHBE.js → ProfilePreview-Bq-DSUM2.js} +2 -2
  47. package/dist/server/assets/{SavedTimelineView-BtCq1dHk.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-CjMIh63G.js → TimelineRouteFrame-Bg3e7X_N.js} +5 -3
  51. package/dist/server/assets/{TweetRichText-DDywESRF.js → TweetRichText-wzUAtbPS.js} +2 -2
  52. package/dist/server/assets/{_tanstack-start-manifest_v-aLWUdbAc.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-D_YkmCK3.js → bookmarks-CPFq3JVX.js} +1 -1
  56. package/dist/server/assets/{client-stream-contracts-Bq6kMmhh.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-DAq6ehL9.js → discuss-BB8wFy4L.js} +3 -3
  59. package/dist/server/assets/{discuss-C1QMBBSk.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-BMm2JWYI.js → likes-CDAjYyTm.js} +1 -1
  66. package/dist/server/assets/{links-dqXb35YM.js → links-DU-TIHtg.js} +6 -6
  67. package/dist/server/assets/{links-Da4tjWtI.js → links-sQ3q2jvO.js} +6 -6
  68. package/dist/server/assets/{mentions-CEyEcOFw.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-DH1TSBkT.js → profile-analyze-BmDeTif5.js} +1 -1
  72. package/dist/server/assets/{profile-analyze-CGCj4VFd.js → profile-analyze-CUfXmXEH.js} +3 -3
  73. package/dist/server/assets/{profiles._handle-B5V-huoo.js → profiles._handle-8snagiR1.js} +4 -4
  74. package/dist/server/assets/{profiles._handle-CJ6rgzt8.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-C4cz9ypd.js → router-DP7iZD-q.js} +421 -125
  77. package/dist/server/assets/{routes-B9aikOdB.js → routes-D9226vKz.js} +1 -1
  78. package/dist/server/assets/start-5Z2QO8AU.js +4 -0
  79. package/dist/server/assets/{today-JkxZXdBt.js → today-BmvIMJzI.js} +3 -3
  80. package/dist/server/assets/{today-B_TA16QG.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-BLXvMGb2.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-DQX6_xc3.js +0 -1
  87. package/dist/client/assets/SavedTimelineView-BhSiVyR1.js +0 -1
  88. package/dist/client/assets/SmartTimestamp-BR0-9se2.js +0 -1
  89. package/dist/client/assets/TimelineRouteFrame-9NDLnXzH.js +0 -1
  90. package/dist/client/assets/blocks-CFp_njvG.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-sR4xMQxY.js +0 -5
  94. package/dist/client/assets/createLucideIcon-JV8isFFR.js +0 -1
  95. package/dist/client/assets/data-sources-euTr6T_L.js +0 -1
  96. package/dist/client/assets/discuss-BWKu_p7B.js +0 -1
  97. package/dist/client/assets/dms-SLXMtC4v.js +0 -1
  98. package/dist/client/assets/inbox-BybSI5Vf.js +0 -1
  99. package/dist/client/assets/index-DZVisyna.js +0 -31
  100. package/dist/client/assets/likes-dyh9p8y_.js +0 -1
  101. package/dist/client/assets/links-CIwqK_dX.js +0 -1
  102. package/dist/client/assets/mapbox-B4zqX7r0.js +0 -2
  103. package/dist/client/assets/network-map-zRysIY_b.js +0 -2
  104. package/dist/client/assets/profile-analyze-C_yDeX6-.js +0 -1
  105. package/dist/client/assets/profiles._handle-DdIVPqU-.js +0 -1
  106. package/dist/client/assets/query-client-fYu2rzRC.js +0 -17
  107. package/dist/client/assets/rate-limits-BWPmPOSh.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-BoO64YoY.js +0 -1
  114. package/dist/client/assets/useTimelineRouteData-BaO3Rna1.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([]);
@@ -9660,6 +10087,16 @@ function ensureStubProfileForXUser(db, externalUserId) {
9660
10087
  function getReferencedTweetId(tweet, type) {
9661
10088
  return tweet.referenced_tweets?.find((item) => item.type === type)?.id ?? null;
9662
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
+ }
9663
10100
  function replaceTweetFts(db, tweetId, text) {
9664
10101
  db.prepare("delete from tweets_fts where tweet_id = ?").run(tweetId);
9665
10102
  db.prepare("insert into tweets_fts (tweet_id, text) values (?, ?)").run(
@@ -9689,7 +10126,7 @@ function ingestTweetPayload(db, {
9689
10126
  created_at = excluded.created_at,
9690
10127
  is_replied = max(tweets.is_replied, excluded.is_replied),
9691
10128
  reply_to_id = coalesce(tweets.reply_to_id, excluded.reply_to_id),
9692
- like_count = excluded.like_count,
10129
+ like_count = case when ? then tweets.like_count else excluded.like_count end,
9693
10130
  media_count = max(tweets.media_count, excluded.media_count),
9694
10131
  entities_json = excluded.entities_json,
9695
10132
  media_json = case
@@ -9710,25 +10147,29 @@ function ingestTweetPayload(db, {
9710
10147
  const tweetIds = [];
9711
10148
  db.transaction(() => {
9712
10149
  const observedAt = (/* @__PURE__ */ new Date()).toISOString();
9713
- 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);
9714
10153
  const author = usersById.get(tweet.author_id);
9715
10154
  const profile = author ? upsertProfileFromXUser(db, author) : ensureStubProfileForXUser(db, tweet.author_id);
9716
10155
  const replyToId = getReferencedTweetId(tweet, "replied_to");
9717
10156
  const quotedTweetId = getReferencedTweetId(tweet, "quoted");
10157
+ const shouldMarkReplied = isPrimaryTweet && markRepliesAsReplied && Boolean(replyToId);
9718
10158
  upsertTweet.run(
9719
10159
  tweet.id,
9720
10160
  profile.profile.id,
9721
10161
  tweet.text,
9722
10162
  tweet.created_at,
9723
- markRepliesAsReplied && replyToId ? 1 : 0,
10163
+ shouldMarkReplied ? 1 : 0,
9724
10164
  replyToId,
9725
10165
  Number(tweet.public_metrics?.like_count ?? 0),
9726
10166
  countTweetMedia(tweet),
9727
10167
  JSON.stringify(tweetEntitiesFromXurl(tweet.entities)),
9728
10168
  buildMediaJsonFromIncludes(tweet, payload.includes?.media),
9729
- quotedTweetId
10169
+ quotedTweetId,
10170
+ !isPrimaryTweet && tweet.public_metrics?.like_count === void 0 ? 1 : 0
9730
10171
  );
9731
- if (edgeKind) {
10172
+ if (edgeKind && isPrimaryTweet) {
9732
10173
  upsertTweetAccountEdge(db, {
9733
10174
  accountId,
9734
10175
  tweetId: tweet.id,
@@ -9738,18 +10179,22 @@ function ingestTweetPayload(db, {
9738
10179
  rawJson: JSON.stringify(tweet)
9739
10180
  });
9740
10181
  }
9741
- upsertCollection?.run(
9742
- accountId,
9743
- tweet.id,
9744
- collectionKind2,
9745
- source,
9746
- JSON.stringify(tweet),
9747
- observedAt
9748
- );
9749
- replaceTweetFts(db, tweet.id, tweet.text);
9750
- tweetIds.push(tweet.id);
9751
- }
9752
- })();
10182
+ if (isPrimaryTweet) {
10183
+ upsertCollection?.run(
10184
+ accountId,
10185
+ tweet.id,
10186
+ collectionKind2,
10187
+ source,
10188
+ JSON.stringify(tweet),
10189
+ observedAt
10190
+ );
10191
+ }
10192
+ replaceTweetFts(db, tweet.id, tweet.text);
10193
+ if (isPrimaryTweet) {
10194
+ tweetIds.push(tweet.id);
10195
+ }
10196
+ }
10197
+ })();
9753
10198
  return tweetIds;
9754
10199
  }
9755
10200
 
@@ -10719,6 +11164,8 @@ var RECENT_TIMELINE_EDGE_CANDIDATES = 5e3;
10719
11164
  function listTimelineItems({
10720
11165
  resource,
10721
11166
  account,
11167
+ listAccountId,
11168
+ listId,
10722
11169
  search,
10723
11170
  replyFilter = "all",
10724
11171
  since,
@@ -10749,7 +11196,7 @@ function listTimelineItems({
10749
11196
  let join3 = "";
10750
11197
  let where = "where e.kind = ?";
10751
11198
  let searchSnippetSelect = "";
10752
- 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";
10753
11200
  if (likedOnly || bookmarkedOnly) {
10754
11201
  if (likedOnly && bookmarkedOnly) {
10755
11202
  timelineEdgesCte = `
@@ -10845,6 +11292,22 @@ function listTimelineItems({
10845
11292
  params.push(until.trim());
10846
11293
  }
10847
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
+ }
10848
11311
  const ftsSearch = search?.trim() ? toFtsSearchQuery(search) : "";
10849
11312
  if (ftsSearch) {
10850
11313
  join3 += " join tweets_fts on tweets_fts.tweet_id = t.id ";
@@ -12518,6 +12981,7 @@ function reachedStartTimeBoundary(payload, startTimeMs) {
12518
12981
  function mergeTimelinePayloads(payloads, limit) {
12519
12982
  const data = [];
12520
12983
  const usersById = /* @__PURE__ */ new Map();
12984
+ const tweetsById = /* @__PURE__ */ new Map();
12521
12985
  const mediaByKey = /* @__PURE__ */ new Map();
12522
12986
  let meta;
12523
12987
  for (const payload of payloads) {
@@ -12530,6 +12994,9 @@ function mergeTimelinePayloads(payloads, limit) {
12530
12994
  for (const user of payload.includes?.users ?? []) {
12531
12995
  usersById.set(user.id, user);
12532
12996
  }
12997
+ for (const tweet of payload.includes?.tweets ?? []) {
12998
+ tweetsById.set(tweet.id, tweet);
12999
+ }
12533
13000
  for (const media of payload.includes?.media ?? []) {
12534
13001
  mediaByKey.set(media.media_key, media);
12535
13002
  }
@@ -12539,6 +13006,7 @@ function mergeTimelinePayloads(payloads, limit) {
12539
13006
  data,
12540
13007
  includes: {
12541
13008
  users: [...usersById.values()],
13009
+ tweets: [...tweetsById.values()],
12542
13010
  media: [...mediaByKey.values()]
12543
13011
  },
12544
13012
  meta
@@ -12847,6 +13315,7 @@ function compactTweet(source, item) {
12847
13315
  createdAt: item.createdAt,
12848
13316
  text: item.text,
12849
13317
  entities: item.entities,
13318
+ media: item.media,
12850
13319
  likeCount: item.likeCount,
12851
13320
  liked: item.liked,
12852
13321
  bookmarked: item.bookmarked,
@@ -12866,6 +13335,7 @@ function compactEmbeddedTweet(item) {
12866
13335
  createdAt: item.createdAt,
12867
13336
  text: item.text,
12868
13337
  entities: item.entities,
13338
+ media: item.media,
12869
13339
  likeCount: item.likeCount ?? 0,
12870
13340
  liked: Boolean(item.liked),
12871
13341
  bookmarked: Boolean(item.bookmarked),
@@ -15390,6 +15860,8 @@ function compactTweet2(source, item) {
15390
15860
  authorProfile: item.author,
15391
15861
  createdAt: item.createdAt,
15392
15862
  text: item.text,
15863
+ entities: item.entities,
15864
+ media: item.media,
15393
15865
  likeCount: item.likeCount,
15394
15866
  liked: item.liked,
15395
15867
  bookmarked: item.bookmarked,
@@ -15427,7 +15899,9 @@ function collectLiveSearchTweets(options) {
15427
15899
  t.created_at,
15428
15900
  t.is_replied,
15429
15901
  t.like_count,
15430
- t.media_count,
15902
+ t.media_count,
15903
+ t.entities_json,
15904
+ t.media_json,
15431
15905
  case
15432
15906
  when exists (
15433
15907
  select 1 from tweet_collections collection
@@ -15481,6 +15955,8 @@ function collectLiveSearchTweets(options) {
15481
15955
  },
15482
15956
  createdAt: String(row.created_at),
15483
15957
  text: String(row.text),
15958
+ entities: parseJsonField2(row.entities_json, {}),
15959
+ media: parseJsonField2(row.media_json, []),
15484
15960
  likeCount: Number(row.like_count),
15485
15961
  liked: Boolean(row.liked),
15486
15962
  bookmarked: Boolean(row.bookmarked),
@@ -23371,19 +23847,550 @@ function registerJobCommands({ program: program2, print: print2 }) {
23371
23847
  });
23372
23848
  }
23373
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
+
23374
24381
  // src/lib/profile-replies.ts
23375
- import { Effect as Effect50 } from "effect";
24382
+ import { Effect as Effect51 } from "effect";
23376
24383
 
23377
24384
  // src/lib/moderation-target.ts
23378
- import { Effect as Effect49 } from "effect";
24385
+ import { Effect as Effect50 } from "effect";
23379
24386
 
23380
24387
  // src/lib/bird-actions.ts
23381
- import { Effect as Effect48 } from "effect";
24388
+ import { Effect as Effect49 } from "effect";
23382
24389
 
23383
24390
  // src/lib/bird-command.ts
23384
24391
  import { access } from "node:fs/promises";
23385
24392
  import { constants as constants2 } from "node:fs";
23386
- import { Data as Data3, Effect as Effect47 } from "effect";
24393
+ import { Data as Data3, Effect as Effect48 } from "effect";
23387
24394
  var BirdCommandUnavailableError = class extends Data3.TaggedError(
23388
24395
  "BirdCommandUnavailableError"
23389
24396
  ) {
@@ -23436,25 +24443,25 @@ function execFailureFromCause(command, cause) {
23436
24443
  }
23437
24444
  function assertBirdCommandAvailableEffect(command) {
23438
24445
  if (!isPathCommand(command)) {
23439
- return Effect47.void;
24446
+ return Effect48.void;
23440
24447
  }
23441
- return Effect47.tryPromise({
24448
+ return Effect48.tryPromise({
23442
24449
  try: () => Promise.resolve(access(command, constants2.X_OK)),
23443
24450
  catch: (cause) => new BirdCommandUnavailableError({
23444
24451
  message: formatBirdInstallHint(command),
23445
24452
  command,
23446
24453
  cause
23447
24454
  })
23448
- }).pipe(Effect47.asVoid);
24455
+ }).pipe(Effect48.asVoid);
23449
24456
  }
23450
24457
  function getBirdCommandEffect() {
23451
- return Effect47.try({
24458
+ return Effect48.try({
23452
24459
  try: () => getBirdCommand(),
23453
24460
  catch: (cause) => cause instanceof Error ? cause : new Error(String(cause))
23454
24461
  });
23455
24462
  }
23456
24463
  function runBirdCommandEffect(args, options) {
23457
- return Effect47.gen(function* () {
24464
+ return Effect48.gen(function* () {
23458
24465
  const birdCommand = yield* getBirdCommandEffect();
23459
24466
  yield* assertBirdCommandAvailableEffect(birdCommand);
23460
24467
  return yield* runSubprocessEffect({
@@ -23467,8 +24474,8 @@ function runBirdCommandEffect(args, options) {
23467
24474
  ...options?.signal ? { signal: options.signal } : {},
23468
24475
  ...options?.killSignal ? { killSignal: options.killSignal } : {}
23469
24476
  }).pipe(
23470
- Effect47.map(({ stdout, stderr }) => ({ stdout, stderr })),
23471
- Effect47.mapError((cause) => execFailureFromCause(birdCommand, cause))
24477
+ Effect48.map(({ stdout, stderr }) => ({ stdout, stderr })),
24478
+ Effect48.mapError((cause) => execFailureFromCause(birdCommand, cause))
23472
24479
  );
23473
24480
  });
23474
24481
  }
@@ -23506,9 +24513,9 @@ function toError18(error) {
23506
24513
  return error instanceof Error ? error : new Error(String(error));
23507
24514
  }
23508
24515
  function runBirdJsonCommandEffect2(args) {
23509
- return Effect48.gen(function* () {
24516
+ return Effect49.gen(function* () {
23510
24517
  const { stdout } = yield* runBirdCommandEffect(args);
23511
- return yield* Effect48.try({
24518
+ return yield* Effect49.try({
23512
24519
  try: () => JSON.parse(stripAnsi2(stdout)),
23513
24520
  catch: toError18
23514
24521
  });
@@ -23522,13 +24529,13 @@ function safeBirdProfileArg(query) {
23522
24529
  throw new Error("Invalid bird profile query");
23523
24530
  }
23524
24531
  function readBirdStatusViaBirdEffect(query) {
23525
- return Effect48.gen(function* () {
23526
- const safeQuery = yield* Effect48.try({
24532
+ return Effect49.gen(function* () {
24533
+ const safeQuery = yield* Effect49.try({
23527
24534
  try: () => safeBirdProfileArg(query),
23528
24535
  catch: toError18
23529
24536
  });
23530
24537
  return yield* runBirdJsonCommandEffect2(["status", safeQuery, "--json"]);
23531
- }).pipe(Effect48.catchAll(() => Effect48.succeed(null)));
24538
+ }).pipe(Effect49.catchAll(() => Effect49.succeed(null)));
23532
24539
  }
23533
24540
  function readBirdStatusViaBird(query) {
23534
24541
  return runEffectPromise(readBirdStatusViaBirdEffect(query));
@@ -23565,8 +24572,8 @@ function toBirdLookupUser(payload) {
23565
24572
  };
23566
24573
  }
23567
24574
  function lookupProfileViaBirdEffect2(query) {
23568
- return Effect48.gen(function* () {
23569
- const safeQuery = yield* Effect48.try({
24575
+ return Effect49.gen(function* () {
24576
+ const safeQuery = yield* Effect49.try({
23570
24577
  try: () => safeBirdProfileArg(query),
23571
24578
  catch: toError18
23572
24579
  });
@@ -23577,11 +24584,11 @@ function lookupProfileViaBirdEffect2(query) {
23577
24584
  "1",
23578
24585
  "--json"
23579
24586
  ]);
23580
- return yield* Effect48.try({
24587
+ return yield* Effect49.try({
23581
24588
  try: () => toBirdLookupUser(payload),
23582
24589
  catch: toError18
23583
24590
  });
23584
- }).pipe(Effect48.catchAll(() => Effect48.succeed(null)));
24591
+ }).pipe(Effect49.catchAll(() => Effect49.succeed(null)));
23585
24592
  }
23586
24593
  function lookupProfileViaBird(query) {
23587
24594
  return runEffectPromise(lookupProfileViaBirdEffect2(query));
@@ -23592,8 +24599,8 @@ function runVerifiedBirdMutationEffect({
23592
24599
  verifyField,
23593
24600
  expectedValue
23594
24601
  }) {
23595
- return Effect48.gen(function* () {
23596
- const safeQuery = yield* Effect48.try({
24602
+ return Effect49.gen(function* () {
24603
+ const safeQuery = yield* Effect49.try({
23597
24604
  try: () => safeBirdProfileArg(query),
23598
24605
  catch: toError18
23599
24606
  });
@@ -23607,12 +24614,12 @@ function runVerifiedBirdMutationEffect({
23607
24614
  action,
23608
24615
  safeQuery
23609
24616
  ]).pipe(
23610
- Effect48.map(({ stdout, stderr }) => ({
24617
+ Effect49.map(({ stdout, stderr }) => ({
23611
24618
  ok: true,
23612
24619
  output: normalizeOutput(stdout, stderr)
23613
24620
  })),
23614
- Effect48.catchAll(
23615
- (error) => Effect48.succeed({
24621
+ Effect49.catchAll(
24622
+ (error) => Effect49.succeed({
23616
24623
  ok: false,
23617
24624
  output: formatExecError2(error, `bird ${action} failed`)
23618
24625
  })
@@ -23691,7 +24698,7 @@ function toError19(error) {
23691
24698
  return error instanceof Error ? error : new Error(String(error));
23692
24699
  }
23693
24700
  function trySync14(try_) {
23694
- return Effect49.try({
24701
+ return Effect50.try({
23695
24702
  try: try_,
23696
24703
  catch: toError19
23697
24704
  });
@@ -23739,11 +24746,11 @@ function resolveLocalProfile(db, normalizedQuery) {
23739
24746
  };
23740
24747
  }
23741
24748
  function resolveProfileEffect(query, providedDb) {
23742
- return Effect49.gen(function* () {
24749
+ return Effect50.gen(function* () {
23743
24750
  const db = providedDb ?? (yield* trySync14(() => getNativeDb()));
23744
24751
  const normalizedQuery = normalizeProfileQuery(query);
23745
24752
  if (!normalizedQuery) {
23746
- return yield* Effect49.fail(new Error("Missing profile handle or id"));
24753
+ return yield* Effect50.fail(new Error("Missing profile handle or id"));
23747
24754
  }
23748
24755
  const local = yield* trySync14(
23749
24756
  () => resolveLocalProfile(db, normalizedQuery)
@@ -23752,7 +24759,7 @@ function resolveProfileEffect(query, providedDb) {
23752
24759
  if (local) {
23753
24760
  return local;
23754
24761
  }
23755
- return yield* Effect49.fail(
24762
+ return yield* Effect50.fail(
23756
24763
  new Error(`Profile not found locally: ${query}`)
23757
24764
  );
23758
24765
  }
@@ -23764,8 +24771,8 @@ function resolveProfileEffect(query, providedDb) {
23764
24771
  const birdResult = yield* tryPromise(
23765
24772
  () => lookupProfileViaBird(local?.profile.handle ?? normalizedQuery)
23766
24773
  ).pipe(
23767
- Effect49.map((value) => ({ ok: true, value })),
23768
- Effect49.catchAll((error) => Effect49.succeed({ ok: false, error }))
24774
+ Effect50.map((value) => ({ ok: true, value })),
24775
+ Effect50.catchAll((error) => Effect50.succeed({ ok: false, error }))
23769
24776
  );
23770
24777
  if (birdResult.ok) {
23771
24778
  user = birdResult.value ?? void 0;
@@ -23776,9 +24783,9 @@ function resolveProfileEffect(query, providedDb) {
23776
24783
  const xurlResult = yield* tryPromise(
23777
24784
  () => /^\d+$/.test(normalizedQuery) ? lookupUsersByIds([normalizedQuery]) : lookupUsersByHandles([local?.profile.handle ?? normalizedQuery])
23778
24785
  ).pipe(
23779
- Effect49.map((value) => ({ ok: true, value })),
23780
- Effect49.catchAll(
23781
- (error) => Effect49.succeed({ ok: false, error })
24786
+ Effect50.map((value) => ({ ok: true, value })),
24787
+ Effect50.catchAll(
24788
+ (error) => Effect50.succeed({ ok: false, error })
23782
24789
  )
23783
24790
  );
23784
24791
  if (xurlResult.ok) {
@@ -23791,13 +24798,13 @@ function resolveProfileEffect(query, providedDb) {
23791
24798
  if (local) {
23792
24799
  return local;
23793
24800
  }
23794
- return yield* Effect49.fail(lastError);
24801
+ return yield* Effect50.fail(lastError);
23795
24802
  }
23796
24803
  if (!user) {
23797
24804
  if (local) {
23798
24805
  return local;
23799
24806
  }
23800
- return yield* Effect49.fail(new Error(`Profile not found: ${query}`));
24807
+ return yield* Effect50.fail(new Error(`Profile not found: ${query}`));
23801
24808
  }
23802
24809
  if (local) {
23803
24810
  return yield* databaseWriteEffect(
@@ -23838,11 +24845,11 @@ function inspectProfileReplies(query, { limit = 12 } = {}) {
23838
24845
  return runEffectPromise(inspectProfileRepliesEffect(query, { limit }));
23839
24846
  }
23840
24847
  function inspectProfileRepliesEffect(query, { limit = 12 } = {}) {
23841
- return Effect50.gen(function* () {
24848
+ return Effect51.gen(function* () {
23842
24849
  const resolved = yield* tryPromise(() => resolveProfile(query));
23843
24850
  const externalUserId = resolved.externalUserId;
23844
24851
  if (!externalUserId) {
23845
- return yield* Effect50.fail(
24852
+ return yield* Effect51.fail(
23846
24853
  new Error(`Profile has no external Twitter user id: ${query}`)
23847
24854
  );
23848
24855
  }
@@ -23937,18 +24944,18 @@ function registerMentionCommands({
23937
24944
  }
23938
24945
 
23939
24946
  // src/lib/blocks.ts
23940
- import { Effect as Effect53 } from "effect";
24947
+ import { Effect as Effect54 } from "effect";
23941
24948
 
23942
24949
  // src/lib/moderation-state.ts
23943
- import { Effect as Effect52 } from "effect";
24950
+ import { Effect as Effect53 } from "effect";
23944
24951
 
23945
24952
  // src/lib/actions-transport.ts
23946
- import { Effect as Effect51 } from "effect";
24953
+ import { Effect as Effect52 } from "effect";
23947
24954
  function toError20(error) {
23948
24955
  return error instanceof Error ? error : new Error(String(error));
23949
24956
  }
23950
24957
  function trySync15(try_) {
23951
- return Effect51.try({
24958
+ return Effect52.try({
23952
24959
  try: try_,
23953
24960
  catch: toError20
23954
24961
  });
@@ -23960,7 +24967,7 @@ function liveWritesDisabled4() {
23960
24967
  return process.env.BIRDCLAW_DISABLE_LIVE_WRITES === "1";
23961
24968
  }
23962
24969
  function verifyExpectedAccountEffect(expectedAccount) {
23963
- return Effect51.gen(function* () {
24970
+ return Effect52.gen(function* () {
23964
24971
  if (!expectedAccount) return null;
23965
24972
  if (liveWritesDisabled4()) return null;
23966
24973
  const sourceUser = yield* tryPromise(() => lookupAuthenticatedUserFresh());
@@ -23970,7 +24977,7 @@ function verifyExpectedAccountEffect(expectedAccount) {
23970
24977
  const expectedHandle = profileHandleKey(expectedAccount.handle);
23971
24978
  if (expectedExternalUserId) {
23972
24979
  if (sourceUserId === expectedExternalUserId) return sourceUserId;
23973
- return yield* Effect51.fail(
24980
+ return yield* Effect52.fail(
23974
24981
  new Error(
23975
24982
  `xurl is authenticated as user ${sourceUserId || "unknown"}, not account ${expectedAccount.id}`
23976
24983
  )
@@ -23978,7 +24985,7 @@ function verifyExpectedAccountEffect(expectedAccount) {
23978
24985
  }
23979
24986
  if (expectedHandle && sourceUsername === expectedHandle)
23980
24987
  return sourceUserId;
23981
- return yield* Effect51.fail(
24988
+ return yield* Effect52.fail(
23982
24989
  new Error(
23983
24990
  sourceUsername ? `xurl is authenticated as @${sourceUsername}, not @${expectedHandle}` : "xurl authenticated user unavailable"
23984
24991
  )
@@ -23986,7 +24993,7 @@ function verifyExpectedAccountEffect(expectedAccount) {
23986
24993
  });
23987
24994
  }
23988
24995
  function runBirdActionEffect(action, query) {
23989
- return Effect51.gen(function* () {
24996
+ return Effect52.gen(function* () {
23990
24997
  const result = yield* tryPromise(
23991
24998
  () => action === "block" ? blockUserViaBird(query) : action === "unblock" ? unblockUserViaBird(query) : action === "mute" ? muteUserViaBird(query) : unmuteUserViaBird(query)
23992
24999
  );
@@ -24006,7 +25013,7 @@ function getVerifyExpectation(action) {
24006
25013
  };
24007
25014
  }
24008
25015
  function runXurlActionEffect(action, query, targetUserId, verifiedSourceUserId) {
24009
- return Effect51.gen(function* () {
25016
+ return Effect52.gen(function* () {
24010
25017
  if (!targetUserId) {
24011
25018
  return {
24012
25019
  ok: false,
@@ -24071,13 +25078,13 @@ function runModerationActionEffect({
24071
25078
  transport,
24072
25079
  expectedAccount
24073
25080
  }) {
24074
- return Effect51.gen(function* () {
25081
+ return Effect52.gen(function* () {
24075
25082
  const requestedTransport = yield* trySync15(
24076
25083
  () => resolveActionsTransport(transport)
24077
25084
  );
24078
25085
  const verifyXurlAccount = () => verifyExpectedAccountEffect(expectedAccount).pipe(
24079
- Effect51.catchAll(
24080
- (error) => Effect51.succeed({
25086
+ Effect52.catchAll(
25087
+ (error) => Effect52.succeed({
24081
25088
  ok: false,
24082
25089
  output: error instanceof Error ? error.message : String(error),
24083
25090
  transport: "xurl"
@@ -24166,7 +25173,7 @@ function toError21(error) {
24166
25173
  return error instanceof Error ? error : new Error(String(error));
24167
25174
  }
24168
25175
  function trySync16(try_) {
24169
- return Effect52.try({
25176
+ return Effect53.try({
24170
25177
  try: try_,
24171
25178
  catch: toError21
24172
25179
  });
@@ -24285,20 +25292,20 @@ function resolveModerationTargetEffect({
24285
25292
  query,
24286
25293
  selfActionError
24287
25294
  }) {
24288
- return Effect52.gen(function* () {
25295
+ return Effect53.gen(function* () {
24289
25296
  const db = yield* trySync16(() => getNativeDb());
24290
25297
  const resolvedAccountId = accountId || getDefaultAccountId(db);
24291
25298
  const accountHandle = yield* trySync16(
24292
25299
  () => getAccountHandle(db, resolvedAccountId)
24293
25300
  );
24294
25301
  if (!accountHandle) {
24295
- return yield* Effect52.fail(
25302
+ return yield* Effect53.fail(
24296
25303
  new Error(`Unknown account: ${resolvedAccountId}`)
24297
25304
  );
24298
25305
  }
24299
25306
  const normalizedQuery = normalizeProfileQuery(query);
24300
25307
  if (normalizedQuery.toLowerCase() === accountHandle.toLowerCase()) {
24301
- return yield* Effect52.fail(new Error(selfActionError));
25308
+ return yield* Effect53.fail(new Error(selfActionError));
24302
25309
  }
24303
25310
  const resolved = yield* resolveProfileEffect(query, db);
24304
25311
  const account = yield* trySync16(
@@ -24309,7 +25316,7 @@ function resolveModerationTargetEffect({
24309
25316
  );
24310
25317
  const accountExternalUserId = account?.external_user_id ?? getExternalUserId(accountProfile?.id ?? "");
24311
25318
  if (profileHandleKey(resolved.profile.handle) === profileHandleKey(accountHandle) || accountProfile?.id && resolved.profile.id === accountProfile.id || accountExternalUserId && resolved.externalUserId === accountExternalUserId) {
24312
- return yield* Effect52.fail(new Error(selfActionError));
25319
+ return yield* Effect53.fail(new Error(selfActionError));
24313
25320
  }
24314
25321
  return {
24315
25322
  db,
@@ -24326,7 +25333,7 @@ function resolveModerationTargetEffect({
24326
25333
  }
24327
25334
  function addModerationEffect(kind, accountId, query, options = {}) {
24328
25335
  const descriptor = moderationDescriptors[kind];
24329
- return Effect52.gen(function* () {
25336
+ return Effect53.gen(function* () {
24330
25337
  const { db, resolved, resolvedAccountId, accountIdentity, actionQuery } = yield* resolveModerationTargetEffect({
24331
25338
  accountId,
24332
25339
  query,
@@ -24376,7 +25383,7 @@ function addModeration(kind, accountId, query, options = {}) {
24376
25383
  }
24377
25384
  function recordModerationEffect(kind, accountId, query) {
24378
25385
  const descriptor = moderationDescriptors[kind];
24379
- return Effect52.gen(function* () {
25386
+ return Effect53.gen(function* () {
24380
25387
  const { db, resolved, resolvedAccountId } = yield* resolveModerationTargetEffect({
24381
25388
  accountId,
24382
25389
  query,
@@ -24407,7 +25414,7 @@ function recordModeration(kind, accountId, query) {
24407
25414
  }
24408
25415
  function removeModerationEffect(kind, accountId, query, options = {}) {
24409
25416
  const descriptor = moderationDescriptors[kind];
24410
- return Effect52.gen(function* () {
25417
+ return Effect53.gen(function* () {
24411
25418
  const { db, resolved, resolvedAccountId, accountIdentity, actionQuery } = yield* resolveModerationTargetEffect({
24412
25419
  accountId,
24413
25420
  query,
@@ -24483,7 +25490,7 @@ function toError22(error) {
24483
25490
  return error instanceof Error ? error : new Error(String(error));
24484
25491
  }
24485
25492
  function trySync17(try_) {
24486
- return Effect53.try({
25493
+ return Effect54.try({
24487
25494
  try: try_,
24488
25495
  catch: toError22
24489
25496
  });
@@ -24496,12 +25503,12 @@ function listBlocks({
24496
25503
  return listModerationState("block", { account, search, limit });
24497
25504
  }
24498
25505
  function syncBlocksEffect(accountId) {
24499
- return Effect53.gen(function* () {
25506
+ return Effect54.gen(function* () {
24500
25507
  const db = yield* trySync17(() => getNativeDb());
24501
25508
  const resolvedAccountId = accountId || getDefaultAccountId(db);
24502
25509
  const accountHandle = getAccountHandle(db, resolvedAccountId);
24503
25510
  if (!accountHandle) {
24504
- return yield* Effect53.fail(
25511
+ return yield* Effect54.fail(
24505
25512
  new Error(`Unknown account: ${resolvedAccountId}`)
24506
25513
  );
24507
25514
  }
@@ -24522,9 +25529,9 @@ function syncBlocksEffect(accountId) {
24522
25529
  }
24523
25530
  };
24524
25531
  }
24525
- return yield* Effect53.gen(function* () {
25532
+ return yield* Effect54.gen(function* () {
24526
25533
  const me = yield* tryPromise(() => lookupAuthenticatedUserFresh()).pipe(
24527
- Effect53.mapError(toError22)
25534
+ Effect54.mapError(toError22)
24528
25535
  );
24529
25536
  const sourceUserId = typeof me?.id === "string" && me.id.length > 0 ? me.id : null;
24530
25537
  const sourceUsername = typeof me?.username === "string" ? me.username.replace(/^@/, "") : "";
@@ -24571,7 +25578,7 @@ function syncBlocksEffect(accountId) {
24571
25578
  do {
24572
25579
  const page = yield* tryPromise(
24573
25580
  () => listBlockedUsers(sourceUserId, nextToken ?? void 0)
24574
- ).pipe(Effect53.mapError(toError22));
25581
+ ).pipe(Effect54.mapError(toError22));
24575
25582
  const pageProfileIds = yield* databaseWriteEffect((writeDb) => {
24576
25583
  return page.items.map((user) => {
24577
25584
  const resolved = upsertProfileFromXUser(writeDb, user);
@@ -24613,8 +25620,8 @@ function syncBlocksEffect(accountId) {
24613
25620
  }
24614
25621
  };
24615
25622
  }).pipe(
24616
- Effect53.catchAll(
24617
- (error) => Effect53.succeed({
25623
+ Effect54.catchAll(
25624
+ (error) => Effect54.succeed({
24618
25625
  ok: false,
24619
25626
  accountId: resolvedAccountId,
24620
25627
  synced: remoteProfileIds.length > 0,
@@ -24765,7 +25772,7 @@ function registerModerationCommands({
24765
25772
 
24766
25773
  // src/lib/blocklist.ts
24767
25774
  import { readFile } from "node:fs/promises";
24768
- import { Effect as Effect54 } from "effect";
25775
+ import { Effect as Effect55 } from "effect";
24769
25776
  function stripInlineCode(value) {
24770
25777
  const match = value.match(/`([^`]+)`/);
24771
25778
  return match?.[1] ?? value;
@@ -24802,15 +25809,15 @@ function parseBlocklistText(text) {
24802
25809
  return items;
24803
25810
  }
24804
25811
  function importBlocklistEffect(accountId, filePath) {
24805
- return Effect54.gen(function* () {
25812
+ return Effect55.gen(function* () {
24806
25813
  const text = yield* tryPromise(() => readFile(filePath, "utf8"));
24807
25814
  const queries = parseBlocklistText(text);
24808
25815
  const items = [];
24809
25816
  for (const query of queries) {
24810
25817
  const blocked = yield* tryPromise(() => addBlock(accountId, query)).pipe(
24811
- Effect54.map((result2) => ({ ok: true, result: result2 })),
24812
- Effect54.catchAll(
24813
- (error) => Effect54.succeed({ ok: false, error })
25818
+ Effect55.map((result2) => ({ ok: true, result: result2 })),
25819
+ Effect55.catchAll(
25820
+ (error) => Effect55.succeed({ ok: false, error })
24814
25821
  )
24815
25822
  );
24816
25823
  if (!blocked.ok) {
@@ -24866,7 +25873,7 @@ function registerModerationCommands2(context) {
24866
25873
  }
24867
25874
 
24868
25875
  // src/lib/link-index.ts
24869
- import { Effect as Effect55 } from "effect";
25876
+ import { Effect as Effect56 } from "effect";
24870
25877
  var DEFAULT_EXPAND_CONCURRENCY = 12;
24871
25878
  function parseJsonField5(value, fallback) {
24872
25879
  if (typeof value !== "string" || value.length === 0) {
@@ -25028,7 +26035,7 @@ function expandWithConcurrencyEffect(db, urls, options) {
25028
26035
  misses: 0,
25029
26036
  errors: 0
25030
26037
  };
25031
- return Effect55.forEach(
26038
+ return Effect56.forEach(
25032
26039
  urls,
25033
26040
  (url) => tryPromise(
25034
26041
  () => expandUrls([url], {
@@ -25037,7 +26044,7 @@ function expandWithConcurrencyEffect(db, urls, options) {
25037
26044
  timeoutMs: options.timeoutMs
25038
26045
  })
25039
26046
  ).pipe(
25040
- Effect55.map((results) => {
26047
+ Effect56.map((results) => {
25041
26048
  const result = results[0];
25042
26049
  if (result.source === "network") {
25043
26050
  counts.networkExpansions++;
@@ -25054,10 +26061,10 @@ function expandWithConcurrencyEffect(db, urls, options) {
25054
26061
  })
25055
26062
  ),
25056
26063
  { concurrency }
25057
- ).pipe(Effect55.as(counts));
26064
+ ).pipe(Effect56.as(counts));
25058
26065
  }
25059
26066
  function backfillLinkIndexEffect(options = {}) {
25060
- return Effect55.gen(function* () {
26067
+ return Effect56.gen(function* () {
25061
26068
  const db = getNativeDb({ seedDemoData: false });
25062
26069
  const { occurrences, entityExpansions } = rebuildOccurrences(
25063
26070
  db,
@@ -25352,7 +26359,7 @@ import {
25352
26359
  } from "node:fs";
25353
26360
  import { appendFile, copyFile, rename, rm, writeFile } from "node:fs/promises";
25354
26361
  import path11 from "node:path";
25355
- import { Effect as Effect56 } from "effect";
26362
+ import { Effect as Effect57 } from "effect";
25356
26363
  var DEFAULT_MAX_BYTES = 100 * 1024 * 1024;
25357
26364
  var PBS_PREFIXES = [
25358
26365
  "/media/",
@@ -25371,13 +26378,13 @@ function toMediaError(error) {
25371
26378
  return error instanceof Error ? error : new Error(String(error));
25372
26379
  }
25373
26380
  function tryMediaSync(try_) {
25374
- return Effect56.try({
26381
+ return Effect57.try({
25375
26382
  try: try_,
25376
26383
  catch: toMediaError
25377
26384
  });
25378
26385
  }
25379
26386
  function tryMediaPromise(try_) {
25380
- return Effect56.tryPromise({
26387
+ return Effect57.tryPromise({
25381
26388
  try: try_,
25382
26389
  catch: toMediaError
25383
26390
  });
@@ -25642,7 +26649,7 @@ function archivePathFor(item, mediaOriginalsDir) {
25642
26649
  return archiveTweetDirs(mediaOriginalsDir, item.tweetId).map((tweetDir) => path11.join(tweetDir, fileName)).find((archivePath) => existsSync5(archivePath)) ?? null;
25643
26650
  }
25644
26651
  function reuseFromArchiveEffect(item, mediaOriginalsDir, maxBytes) {
25645
- return Effect56.gen(function* () {
26652
+ return Effect57.gen(function* () {
25646
26653
  const archivePath = archivePathFor(item, mediaOriginalsDir);
25647
26654
  if (!archivePath || !existsSync5(archivePath)) return null;
25648
26655
  const bytes = fileSize(archivePath);
@@ -25669,15 +26676,15 @@ function contentRangeTotal(response) {
25669
26676
  return total ? Number(total) : null;
25670
26677
  }
25671
26678
  function writeResponseBodyEffect(response, tmpPath, append, maxBytes, initialBytes) {
25672
- return Effect56.gen(function* () {
26679
+ return Effect57.gen(function* () {
25673
26680
  if (!response.body)
25674
- return yield* Effect56.fail(new Error("missing response body"));
26681
+ return yield* Effect57.fail(new Error("missing response body"));
25675
26682
  let bytes = 0;
25676
26683
  if (!append) {
25677
26684
  yield* tryMediaPromise(() => writeFile(tmpPath, Buffer.alloc(0)));
25678
26685
  }
25679
26686
  const reader = response.body.getReader();
25680
- const result = yield* Effect56.gen(function* () {
26687
+ const result = yield* Effect57.gen(function* () {
25681
26688
  for (; ; ) {
25682
26689
  const { done, value } = yield* tryMediaPromise(() => reader.read());
25683
26690
  if (done) {
@@ -25687,31 +26694,31 @@ function writeResponseBodyEffect(response, tmpPath, append, maxBytes, initialByt
25687
26694
  bytes += chunk.length;
25688
26695
  if (initialBytes + bytes > maxBytes) {
25689
26696
  yield* tryMediaPromise(() => rm(tmpPath, { force: true })).pipe(
25690
- Effect56.catchAll(() => Effect56.void)
26697
+ Effect57.catchAll(() => Effect57.void)
25691
26698
  );
25692
- return yield* Effect56.fail(new Error("max-bytes"));
26699
+ return yield* Effect57.fail(new Error("max-bytes"));
25693
26700
  }
25694
26701
  yield* tryMediaPromise(() => appendFile(tmpPath, chunk));
25695
26702
  }
25696
26703
  return bytes;
25697
26704
  }).pipe(
25698
- Effect56.map((writtenBytes) => ({ ok: true, writtenBytes })),
25699
- Effect56.catchAll(
25700
- (error) => Effect56.gen(function* () {
26705
+ Effect57.map((writtenBytes) => ({ ok: true, writtenBytes })),
26706
+ Effect57.catchAll(
26707
+ (error) => Effect57.gen(function* () {
25701
26708
  if (error.message === "max-bytes") {
25702
26709
  yield* tryMediaPromise(() => rm(tmpPath, { force: true })).pipe(
25703
- Effect56.catchAll(() => Effect56.void)
26710
+ Effect57.catchAll(() => Effect57.void)
25704
26711
  );
25705
26712
  }
25706
26713
  yield* tryMediaPromise(() => reader.cancel(error)).pipe(
25707
- Effect56.catchAll(() => Effect56.void)
26714
+ Effect57.catchAll(() => Effect57.void)
25708
26715
  );
25709
26716
  return { error, ok: false };
25710
26717
  })
25711
26718
  ),
25712
- Effect56.ensuring(Effect56.sync(() => reader.releaseLock()))
26719
+ Effect57.ensuring(Effect57.sync(() => reader.releaseLock()))
25713
26720
  );
25714
- if (!result.ok) return yield* Effect56.fail(result.error);
26721
+ if (!result.ok) return yield* Effect57.fail(result.error);
25715
26722
  return result.writtenBytes;
25716
26723
  });
25717
26724
  }
@@ -25723,13 +26730,13 @@ function fetchOneEffect({
25723
26730
  userAgent,
25724
26731
  maxBytes
25725
26732
  }) {
25726
- return Effect56.gen(function* () {
26733
+ return Effect57.gen(function* () {
25727
26734
  let rateLimited = false;
25728
26735
  for (let attempt = 0; attempt <= retryMax; attempt += 1) {
25729
26736
  const partialBytes = item.kind === "image" ? 0 : fileSize(item.tmpPath);
25730
26737
  if (partialBytes > maxBytes) {
25731
26738
  yield* tryMediaPromise(() => rm(item.tmpPath, { force: true })).pipe(
25732
- Effect56.catchAll(() => Effect56.void)
26739
+ Effect57.catchAll(() => Effect57.void)
25733
26740
  );
25734
26741
  return fail(item, "max-bytes");
25735
26742
  }
@@ -25741,9 +26748,9 @@ function fetchOneEffect({
25741
26748
  }
25742
26749
  })
25743
26750
  ).pipe(
25744
- Effect56.map((response2) => ({ ok: true, response: response2 })),
25745
- Effect56.catchAll(
25746
- (error) => Effect56.succeed({ error, ok: false })
26751
+ Effect57.map((response2) => ({ ok: true, response: response2 })),
26752
+ Effect57.catchAll(
26753
+ (error) => Effect57.succeed({ error, ok: false })
25747
26754
  )
25748
26755
  );
25749
26756
  if (!responseResult.ok) {
@@ -25754,7 +26761,7 @@ function fetchOneEffect({
25754
26761
  rateLimited = true;
25755
26762
  if (attempt < retryMax) {
25756
26763
  yield* tryMediaPromise(() => sleep(1e3 * 2 ** attempt)).pipe(
25757
- Effect56.catchAll(() => Effect56.void)
26764
+ Effect57.catchAll(() => Effect57.void)
25758
26765
  );
25759
26766
  continue;
25760
26767
  }
@@ -25766,7 +26773,7 @@ function fetchOneEffect({
25766
26773
  const expectedTotal = contentRangeTotal(response) ?? (contentLength(response) ?? 0) + (response.status === 206 ? partialBytes : 0);
25767
26774
  if (expectedTotal > maxBytes) {
25768
26775
  yield* tryMediaPromise(() => rm(item.tmpPath, { force: true })).pipe(
25769
- Effect56.catchAll(() => Effect56.void)
26776
+ Effect57.catchAll(() => Effect57.void)
25770
26777
  );
25771
26778
  return fail(item, "max-bytes");
25772
26779
  }
@@ -25778,9 +26785,9 @@ function fetchOneEffect({
25778
26785
  maxBytes,
25779
26786
  append ? partialBytes : 0
25780
26787
  ).pipe(
25781
- Effect56.map((bytes) => ({ bytes, ok: true })),
25782
- Effect56.catchAll(
25783
- (error) => Effect56.succeed({ error, ok: false })
26788
+ Effect57.map((bytes) => ({ bytes, ok: true })),
26789
+ Effect57.catchAll(
26790
+ (error) => Effect57.succeed({ error, ok: false })
25784
26791
  )
25785
26792
  );
25786
26793
  if (!bytesResult.ok) {
@@ -25854,7 +26861,7 @@ function runGroupEffect(items, parallel, pacingMs, now2, sleep, worker) {
25854
26861
  );
25855
26862
  })
25856
26863
  );
25857
- return Effect56.forEach(pacedItems, runPaced, {
26864
+ return Effect57.forEach(pacedItems, runPaced, {
25858
26865
  concurrency: Math.min(parallel, items.length),
25859
26866
  discard: true
25860
26867
  });
@@ -25863,7 +26870,7 @@ function fetchTweetMedia(options = {}) {
25863
26870
  return runEffectPromise(fetchTweetMediaEffect(options));
25864
26871
  }
25865
26872
  function fetchTweetMediaEffect(options = {}) {
25866
- return Effect56.gen(function* () {
26873
+ return Effect57.gen(function* () {
25867
26874
  const now2 = options.now ?? Date.now;
25868
26875
  const startedAt = now2();
25869
26876
  const sleep = options.sleep ?? defaultSleep;
@@ -25930,7 +26937,7 @@ function fetchTweetMediaEffect(options = {}) {
25930
26937
  retryMax,
25931
26938
  userAgent,
25932
26939
  maxBytes
25933
- }).pipe(Effect56.map((fetched) => applyFetched(result, fetched)))
26940
+ }).pipe(Effect57.map((fetched) => applyFetched(result, fetched)))
25934
26941
  );
25935
26942
  yield* runGroupEffect(
25936
26943
  httpCandidates.filter((item) => item.kind === "image"),
@@ -25978,9 +26985,9 @@ function formatMediaFetchResult(result) {
25978
26985
  }
25979
26986
 
25980
26987
  // src/lib/whois.ts
25981
- import { Effect as Effect57 } from "effect";
26988
+ import { Effect as Effect58 } from "effect";
25982
26989
  function trySync18(try_) {
25983
- return Effect57.try({
26990
+ return Effect58.try({
25984
26991
  try: try_,
25985
26992
  catch: (cause) => cause
25986
26993
  });
@@ -26511,7 +27518,7 @@ function loadWhoisConversations(query, options, includeDms, context, limit) {
26511
27518
  return [...merged.values()];
26512
27519
  }
26513
27520
  function runWhoisEffect(query, options = {}) {
26514
- return Effect57.gen(function* () {
27521
+ return Effect58.gen(function* () {
26515
27522
  const includeDms = options.dms ?? true;
26516
27523
  const includeTweets = options.tweets ?? false;
26517
27524
  const limit = options.limit ?? 10;
@@ -26758,7 +27765,7 @@ function registerSearchCommands({
26758
27765
  parsePositiveIntegerOption: parsePositiveIntegerOption2
26759
27766
  }) {
26760
27767
  const searchCommand = program2.command("search").description("Search local data");
26761
- 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(
26762
27769
  "--min-likes <n>",
26763
27770
  "Override the low-quality like threshold (default 50)"
26764
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) => {
@@ -26768,10 +27775,18 @@ function registerSearchCommands({
26768
27775
  );
26769
27776
  if (options.minLikes !== void 0 && minLikes === void 0) return;
26770
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;
26771
27783
  const replyFilter = options.replied ? "replied" : options.unreplied ? "unreplied" : "all";
26772
27784
  print2(
26773
27785
  listTimelineItems({
26774
27786
  resource: options.resource === "mentions" ? "mentions" : options.resource === "authored" ? "authored" : "home",
27787
+ account: options.account,
27788
+ listAccountId: selectedList?.accountId,
27789
+ listId: selectedList?.listId,
26775
27790
  search: query,
26776
27791
  replyFilter,
26777
27792
  since: options.since,
@@ -26954,7 +27969,7 @@ import { Readable as Readable2 } from "node:stream";
26954
27969
  import { pathToFileURL } from "node:url";
26955
27970
 
26956
27971
  // src/lib/http-effect.ts
26957
- import { Effect as Effect58 } from "effect";
27972
+ import { Effect as Effect59 } from "effect";
26958
27973
  var LOCAL_WEB_PEER_HEADER = "x-birdclaw-local-peer";
26959
27974
 
26960
27975
  // src/lib/production-server.ts
@@ -27228,7 +28243,7 @@ function registerStorageCommands({
27228
28243
  }
27229
28244
 
27230
28245
  // src/lib/authored-live.ts
27231
- import { Effect as Effect59 } from "effect";
28246
+ import { Effect as Effect60 } from "effect";
27232
28247
  var AuthoredSyncError = class extends Error {
27233
28248
  constructor(message, exitCode) {
27234
28249
  super(message);
@@ -27470,10 +28485,10 @@ function resolveAuthoredIdentityEffect({
27470
28485
  account,
27471
28486
  db
27472
28487
  }) {
27473
- return Effect59.gen(function* () {
28488
+ return Effect60.gen(function* () {
27474
28489
  const status = yield* liveTransportGateway.xurl.getTransportStatus();
27475
28490
  if (status.availableTransport !== "xurl") {
27476
- return yield* Effect59.fail(new AuthoredSyncError(status.statusText, 4));
28491
+ return yield* Effect60.fail(new AuthoredSyncError(status.statusText, 4));
27477
28492
  }
27478
28493
  const resolvedAccount = yield* trySync(
27479
28494
  () => resolveLiveSyncAccount(db, account)
@@ -27488,7 +28503,7 @@ function resolveAuthoredIdentityEffect({
27488
28503
  const authenticated = yield* liveTransportGateway.xurl.lookupAuthenticatedUser();
27489
28504
  const authenticatedUser = userFromAuthenticatedPayload(authenticated);
27490
28505
  if (!authenticatedUser?.id) {
27491
- return yield* Effect59.fail(
28506
+ return yield* Effect60.fail(
27492
28507
  new AuthoredSyncError(
27493
28508
  "Could not resolve authenticated Twitter user id",
27494
28509
  4
@@ -27496,7 +28511,7 @@ function resolveAuthoredIdentityEffect({
27496
28511
  );
27497
28512
  }
27498
28513
  if (normalizeUsername(authenticatedUser.username) !== normalizeUsername(resolvedAccount.username)) {
27499
- return yield* Effect59.fail(
28514
+ return yield* Effect60.fail(
27500
28515
  new AuthoredSyncError(
27501
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.`,
27502
28517
  4
@@ -27795,9 +28810,9 @@ function syncAuthoredTweetsEffect({
27795
28810
  sinceId,
27796
28811
  untilId
27797
28812
  }) {
27798
- return Effect59.gen(function* () {
28813
+ return Effect60.gen(function* () {
27799
28814
  if (mode !== "xurl") {
27800
- return yield* Effect59.fail(
28815
+ return yield* Effect60.fail(
27801
28816
  new Error("authored sync only supports --mode xurl")
27802
28817
  );
27803
28818
  }
@@ -27860,7 +28875,7 @@ function syncAuthoredTweetsEffect({
27860
28875
  sourceUser
27861
28876
  })
27862
28877
  ).pipe(
27863
- Effect59.flatMap(
28878
+ Effect60.flatMap(
27864
28879
  () => trySync(() => {
27865
28880
  newestSeenId = maxTweetId(
27866
28881
  newestSeenId,
@@ -27963,6 +28978,32 @@ function registerSyncCommands({
27963
28978
  autoSyncAfterWrite: autoSyncAfterWrite2
27964
28979
  }) {
27965
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
+ });
27966
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) => {
27967
29008
  const result = await syncHomeTimeline({
27968
29009
  account: options.account,
@@ -28137,6 +29178,7 @@ registerMentionCommands(commandContext);
28137
29178
  registerDirectMessageCommands(commandContext);
28138
29179
  registerSyncCommands(commandContext);
28139
29180
  registerJobCommands(commandContext);
29181
+ registerListCommands(commandContext);
28140
29182
  registerModerationCommands2(commandContext);
28141
29183
  registerComposeCommands(commandContext);
28142
29184
  registerInboxCommand(commandContext);