birdclaw 0.8.5 → 0.9.1

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 (81) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +23 -3
  3. package/dist/cli/birdclaw.js +1344 -190
  4. package/dist/client/assets/{AvatarChip-DLoqM5xD.js → AvatarChip-BN3b4MhM.js} +1 -1
  5. package/dist/client/assets/{ProfilePreview-DQX6_xc3.js → ProfilePreview-CHbT0U9a.js} +1 -1
  6. package/dist/client/assets/{SavedTimelineView-BhSiVyR1.js → SavedTimelineView-mGXE9EYJ.js} +1 -1
  7. package/dist/client/assets/{SmartTimestamp-BR0-9se2.js → SmartTimestamp-DG8XJKUz.js} +1 -1
  8. package/dist/client/assets/TimelineRouteFrame-BmMC7qLj.js +1 -0
  9. package/dist/client/assets/blocks-CawnB0mN.js +1 -0
  10. package/dist/client/assets/{bookmarks-LbYOsoKf.js → bookmarks-DZIMTKEp.js} +1 -1
  11. package/dist/client/assets/client-stream-contracts-DcKcyZhg.js +5 -0
  12. package/dist/client/assets/{data-sources-euTr6T_L.js → data-sources-BJUT2qaO.js} +1 -1
  13. package/dist/client/assets/{discuss-BWKu_p7B.js → discuss-DMPJVaRk.js} +1 -1
  14. package/dist/client/assets/dms-BfPFG4Qr.js +1 -0
  15. package/dist/client/assets/{inbox-BybSI5Vf.js → inbox-DmeYJ7tt.js} +1 -1
  16. package/dist/client/assets/{index-DZVisyna.js → index-Bqmo4MbK.js} +6 -6
  17. package/dist/client/assets/likes-BxVL-j_C.js +1 -0
  18. package/dist/client/assets/{links-CIwqK_dX.js → links-BeA1Wt8F.js} +1 -1
  19. package/dist/client/assets/{mapbox-B4zqX7r0.js → mapbox-BTSMNSry.js} +2 -2
  20. package/dist/client/assets/{mapbox-gl-DYNcLcll.js → mapbox-gl-CPemBIlO.js} +1 -1
  21. package/dist/client/assets/{mentions-BoVVAIT7.js → mentions-DBrY-UPg.js} +1 -1
  22. package/dist/client/assets/{network-map-zRysIY_b.js → network-map-MqbE164r.js} +2 -2
  23. package/dist/client/assets/{profile-analyze-C_yDeX6-.js → profile-analyze-DEA75Reh.js} +1 -1
  24. package/dist/client/assets/{profile-hydration-client-YEtJsNTv.js → profile-hydration-client-BNe0sUmK.js} +1 -1
  25. package/dist/client/assets/{profiles._handle-DdIVPqU-.js → profiles._handle-K34ofz9U.js} +1 -1
  26. package/dist/client/assets/{query-client-fYu2rzRC.js → query-client-DUP1ZzP7.js} +1 -1
  27. package/dist/client/assets/{rate-limits-BWPmPOSh.js → rate-limits-B8Wmpt9r.js} +1 -1
  28. package/dist/client/assets/{routes-CiLuYpZr.js → routes-Bl-Lw2hN.js} +1 -1
  29. package/dist/client/assets/schemas-CaALBBpc.js +64 -0
  30. package/dist/client/assets/styles-zihNNCGg.css +2 -0
  31. package/dist/client/assets/today-DFvZ0Ys5.js +1 -0
  32. package/dist/client/assets/{useTimelineRouteData-BaO3Rna1.js → useTimelineRouteData-Cpf-Jqsx.js} +1 -1
  33. package/dist/server/assets/{AvatarChip-zUDAEmjL.js → AvatarChip-DWag3IzT.js} +1 -1
  34. package/dist/server/assets/{FeedState-ntW0Q2GB.js → FeedState-CwzcvP-p.js} +1 -1
  35. package/dist/server/assets/{ProfileAnalysisStream-L7bs6IHH.js → ProfileAnalysisStream-CLjrYzEx.js} +2 -2
  36. package/dist/server/assets/{ProfilePreview-Cg-AOHBE.js → ProfilePreview-Bq-DSUM2.js} +2 -2
  37. package/dist/server/assets/{SavedTimelineView-BtCq1dHk.js → SavedTimelineView-Bk9nfnb-.js} +2 -2
  38. package/dist/server/assets/{SmartTimestamp-DfwE1NGf.js → SmartTimestamp-C06w_f9J.js} +1 -1
  39. package/dist/server/assets/SyncNowButton-CKuXKYcd.js +259 -0
  40. package/dist/server/assets/{TimelineRouteFrame-CjMIh63G.js → TimelineRouteFrame-Bg3e7X_N.js} +5 -3
  41. package/dist/server/assets/{TweetRichText-DDywESRF.js → TweetRichText-wzUAtbPS.js} +2 -2
  42. package/dist/server/assets/{_tanstack-start-manifest_v-aLWUdbAc.js → _tanstack-start-manifest_v-CVfs07C8.js} +24 -24
  43. package/dist/server/assets/{blocks-DSgQghqP.js → blocks-CaP_u3mY.js} +3 -3
  44. package/dist/server/assets/{blocks-C1mgGTB3.js → blocks-DKy4yTF-.js} +3 -3
  45. package/dist/server/assets/{bookmarks-D_YkmCK3.js → bookmarks-CPFq3JVX.js} +1 -1
  46. package/dist/server/assets/{client-stream-contracts-Bq6kMmhh.js → client-stream-contracts-gtXEreFY.js} +72 -8
  47. package/dist/server/assets/{data-sources-BnA1Q94p.js → data-sources-DWwmQqcO.js} +1 -1
  48. package/dist/server/assets/{discuss-DAq6ehL9.js → discuss-BB8wFy4L.js} +3 -3
  49. package/dist/server/assets/{discuss-C1QMBBSk.js → discuss-CVGB4_I0.js} +3 -3
  50. package/dist/server/assets/{dms-DAyQrVM2.js → dms-7NzdyQZQ.js} +6 -6
  51. package/dist/server/assets/{dms-BuLj4bp8.js → dms-vbEumuuO.js} +4 -4
  52. package/dist/server/assets/{inbox-CkGOaW1q.js → inbox-CBeZctFd.js} +2 -2
  53. package/dist/server/assets/{inbox-DpWd1EzC.js → inbox-UpHhXODd.js} +4 -4
  54. package/dist/server/assets/{likes-BMm2JWYI.js → likes-CDAjYyTm.js} +1 -1
  55. package/dist/server/assets/{links-dqXb35YM.js → links-DU-TIHtg.js} +6 -6
  56. package/dist/server/assets/{links-Da4tjWtI.js → links-sQ3q2jvO.js} +6 -6
  57. package/dist/server/assets/{mentions-CEyEcOFw.js → mentions-DdndGSRw.js} +1 -1
  58. package/dist/server/assets/{network-map-D3CzRcC4.js → network-map-B9iOapuB.js} +2 -2
  59. package/dist/server/assets/{network-map-D5DoLOwR.js → network-map-CcOz-T9O.js} +2 -2
  60. package/dist/server/assets/{profile-analyze-DH1TSBkT.js → profile-analyze-BmDeTif5.js} +1 -1
  61. package/dist/server/assets/{profile-analyze-CGCj4VFd.js → profile-analyze-CUfXmXEH.js} +3 -3
  62. package/dist/server/assets/{profiles._handle-B5V-huoo.js → profiles._handle-8snagiR1.js} +4 -4
  63. package/dist/server/assets/{profiles._handle-CJ6rgzt8.js → profiles._handle-BAqnghwo.js} +5 -5
  64. package/dist/server/assets/{rate-limits-VAr2qzYD.js → rate-limits-BY2-aQPT.js} +1 -1
  65. package/dist/server/assets/{router-C4cz9ypd.js → router-CAvZOdfA.js} +437 -108
  66. package/dist/server/assets/{routes-B9aikOdB.js → routes-D9226vKz.js} +1 -1
  67. package/dist/server/assets/{today-JkxZXdBt.js → today-BmvIMJzI.js} +3 -3
  68. package/dist/server/assets/{today-B_TA16QG.js → today-DKWphGrC.js} +112 -57
  69. package/dist/server/assets/{ui-qbmcMSSX.js → ui-rDHeS-cc.js} +2 -1
  70. package/dist/server/assets/{useTimelineRouteData-BLXvMGb2.js → useTimelineRouteData-BmDbGvQ8.js} +7 -7
  71. package/dist/server/server.js +2 -2
  72. package/package.json +13 -13
  73. package/dist/client/assets/TimelineRouteFrame-9NDLnXzH.js +0 -1
  74. package/dist/client/assets/blocks-CFp_njvG.js +0 -1
  75. package/dist/client/assets/client-stream-contracts-sR4xMQxY.js +0 -5
  76. package/dist/client/assets/dms-SLXMtC4v.js +0 -1
  77. package/dist/client/assets/likes-dyh9p8y_.js +0 -1
  78. package/dist/client/assets/schemas-I9r6F6ya.js +0 -64
  79. package/dist/client/assets/styles-B8s3tt3r.css +0 -2
  80. package/dist/client/assets/today-BoO64YoY.js +0 -1
  81. package/dist/server/assets/SyncNowButton-BOn_IBXS.js +0 -76
@@ -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(
@@ -4811,6 +5043,18 @@ var DEFAULT_DELIMITER_HOLD = 8;
4811
5043
  function toError3(error) {
4812
5044
  return error instanceof Error ? error : new Error(String(error));
4813
5045
  }
5046
+ var DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
5047
+ function resolveOpenAIBaseUrl(getEnv2) {
5048
+ const configured = getEnv2("BIRDCLAW_OPENAI_BASE_URL") || getEnv2("OPENAI_BASE_URL");
5049
+ const base = configured?.trim() || DEFAULT_OPENAI_BASE_URL;
5050
+ return base.replace(/\/+$/, "");
5051
+ }
5052
+ function debugLog(getEnv2, message) {
5053
+ if (!getEnv2("BIRDCLAW_DEBUG")) return;
5054
+ if (typeof process === "undefined") return;
5055
+ process.stderr.write(`[birdclaw:openai] ${message}
5056
+ `);
5057
+ }
4814
5058
  function createOpenAIStreamState() {
4815
5059
  return {
4816
5060
  eventBuffer: "",
@@ -4949,8 +5193,11 @@ function requestOpenAIResponseEffect({
4949
5193
  if (!apiKey) {
4950
5194
  return yield* Effect6.fail(new Error("OPENAI_API_KEY is not set"));
4951
5195
  }
5196
+ const baseUrl = resolveOpenAIBaseUrl(runtime.env);
5197
+ const url = `${baseUrl}/responses`;
5198
+ debugLog(runtime.env, `POST ${url}`);
4952
5199
  const response = yield* tryPromise(
4953
- () => runtime.fetch("https://api.openai.com/v1/responses", {
5200
+ () => runtime.fetch(url, {
4954
5201
  method: "POST",
4955
5202
  signal,
4956
5203
  headers: {
@@ -4959,17 +5206,29 @@ function requestOpenAIResponseEffect({
4959
5206
  },
4960
5207
  body: JSON.stringify(body)
4961
5208
  })
4962
- ).pipe(Effect6.mapError(toError3));
5209
+ ).pipe(
5210
+ Effect6.mapError(toError3),
5211
+ Effect6.tapError(
5212
+ (error) => Effect6.sync(
5213
+ () => debugLog(runtime.env, `network error for ${url}: ${error.message}`)
5214
+ )
5215
+ )
5216
+ );
4963
5217
  if (!response.ok) {
4964
5218
  const text = yield* tryPromise(() => response.text()).pipe(
4965
5219
  Effect6.mapError(toError3)
4966
5220
  );
5221
+ debugLog(
5222
+ runtime.env,
5223
+ `${url} -> ${String(response.status)} ${text.slice(0, 400)}`
5224
+ );
4967
5225
  return yield* Effect6.fail(
4968
5226
  new Error(
4969
5227
  `OpenAI request failed: ${String(response.status)} ${text.slice(0, 400)}`
4970
5228
  )
4971
5229
  );
4972
5230
  }
5231
+ debugLog(runtime.env, `${url} -> ${String(response.status)} OK`);
4973
5232
  return response;
4974
5233
  });
4975
5234
  }
@@ -6133,6 +6392,16 @@ function runBirdJsonCommandAllowFailureEffect(args, timeoutMs) {
6133
6392
  })
6134
6393
  );
6135
6394
  }
6395
+ function runBirdTweetJsonCommandEffect(args, timeoutMs) {
6396
+ return runBirdJsonCommandEffect([...args, "--json-full"], timeoutMs).pipe(
6397
+ Effect8.catchAll((error) => {
6398
+ if (!isUnsupportedBirdOptionError(error, "--json-full")) {
6399
+ return Effect8.fail(error);
6400
+ }
6401
+ return runBirdJsonCommandEffect([...args, "--json"], timeoutMs);
6402
+ })
6403
+ );
6404
+ }
6136
6405
  function getBirdTweetItems(payload, command) {
6137
6406
  if (Array.isArray(payload)) {
6138
6407
  return payload;
@@ -6197,19 +6466,63 @@ function toReferencedTweets(item) {
6197
6466
  }
6198
6467
  return references.length > 0 ? references : void 0;
6199
6468
  }
6469
+ function getRecord(value) {
6470
+ return value && typeof value === "object" ? value : void 0;
6471
+ }
6472
+ function findGraphqlAvatarUrl(value, authorId) {
6473
+ const pending = [value];
6474
+ const seen = /* @__PURE__ */ new Set();
6475
+ while (pending.length > 0) {
6476
+ const current = pending.pop();
6477
+ const record3 = getRecord(current);
6478
+ if (!record3 || seen.has(record3)) continue;
6479
+ seen.add(record3);
6480
+ if (record3.rest_id === authorId) {
6481
+ const avatar = getRecord(record3.avatar);
6482
+ if (typeof avatar?.image_url === "string" && avatar.image_url) {
6483
+ return avatar.image_url;
6484
+ }
6485
+ }
6486
+ for (const child of Object.values(record3)) {
6487
+ if (child && typeof child === "object") {
6488
+ pending.push(child);
6489
+ }
6490
+ }
6491
+ }
6492
+ return void 0;
6493
+ }
6494
+ function isHydratedBirdTweetItem(value) {
6495
+ const item = value;
6496
+ return typeof item?.id === "string" && typeof item.text === "string" && typeof item.createdAt === "string";
6497
+ }
6200
6498
  function normalizeBirdTweets(items) {
6201
6499
  const users = /* @__PURE__ */ new Map();
6202
- const data = items.map((item) => {
6500
+ const includedTweets = /* @__PURE__ */ new Map();
6501
+ const normalizeItem = (item, preserveMissingMetrics = false) => {
6203
6502
  const authorId = String(
6204
6503
  item.authorId ?? item.author?.username ?? "unknown"
6205
6504
  );
6206
- if (!users.has(authorId)) {
6505
+ const profileImageUrl = item.author?.profileImageUrl ?? findGraphqlAvatarUrl(item._raw, authorId);
6506
+ const existingUser = users.get(authorId);
6507
+ if (!existingUser) {
6207
6508
  users.set(authorId, {
6208
6509
  id: authorId,
6209
6510
  username: item.author?.username ?? `user_${authorId}`,
6210
- name: item.author?.name ?? item.author?.username ?? `user_${authorId}`
6511
+ name: item.author?.name ?? item.author?.username ?? `user_${authorId}`,
6512
+ ...profileImageUrl ? { profile_image_url: profileImageUrl } : {}
6513
+ });
6514
+ } else if (profileImageUrl && !existingUser.profile_image_url) {
6515
+ users.set(authorId, {
6516
+ ...existingUser,
6517
+ profile_image_url: profileImageUrl
6211
6518
  });
6212
6519
  }
6520
+ if (isHydratedBirdTweetItem(item.quotedTweet)) {
6521
+ const quotedTweet = normalizeItem(item.quotedTweet, true);
6522
+ if (quotedTweet.id !== item.id) {
6523
+ includedTweets.set(quotedTweet.id, quotedTweet);
6524
+ }
6525
+ }
6213
6526
  return {
6214
6527
  id: item.id,
6215
6528
  author_id: authorId,
@@ -6218,17 +6531,29 @@ function normalizeBirdTweets(items) {
6218
6531
  conversation_id: item.conversationId ?? item.id,
6219
6532
  entities: toTweetEntities(item),
6220
6533
  referenced_tweets: toReferencedTweets(item),
6221
- public_metrics: {
6534
+ public_metrics: preserveMissingMetrics ? {
6535
+ ...item.replyCount === void 0 ? {} : { reply_count: Number(item.replyCount) },
6536
+ ...item.retweetCount === void 0 ? {} : { retweet_count: Number(item.retweetCount) },
6537
+ ...item.likeCount === void 0 ? {} : { like_count: Number(item.likeCount) }
6538
+ } : {
6222
6539
  reply_count: Number(item.replyCount ?? 0),
6223
6540
  retweet_count: Number(item.retweetCount ?? 0),
6224
6541
  like_count: Number(item.likeCount ?? 0)
6225
6542
  },
6226
6543
  edit_history_tweet_ids: [item.id]
6227
6544
  };
6228
- });
6545
+ };
6546
+ const data = items.map((item) => normalizeItem(item));
6547
+ const includes = {};
6548
+ if (users.size > 0) {
6549
+ includes.users = Array.from(users.values());
6550
+ }
6551
+ if (includedTweets.size > 0) {
6552
+ includes.tweets = Array.from(includedTweets.values());
6553
+ }
6229
6554
  return {
6230
6555
  data,
6231
- includes: users.size > 0 ? { users: Array.from(users.values()) } : void 0,
6556
+ includes: users.size > 0 || includedTweets.size > 0 ? includes : void 0,
6232
6557
  meta: {
6233
6558
  result_count: data.length,
6234
6559
  page_count: 1,
@@ -6260,11 +6585,10 @@ function listMentionsViaBirdEffect({
6260
6585
  maxResults
6261
6586
  }) {
6262
6587
  return Effect8.gen(function* () {
6263
- const stdout = yield* runBirdJsonCommandEffect([
6588
+ const stdout = yield* runBirdTweetJsonCommandEffect([
6264
6589
  "mentions",
6265
6590
  "-n",
6266
- String(maxResults),
6267
- "--json"
6591
+ String(maxResults)
6268
6592
  ]);
6269
6593
  const payload = yield* parseBirdJsonEffect(stdout);
6270
6594
  return yield* normalizeBirdTweetsPayloadEffect(payload, "mentions");
@@ -6277,14 +6601,14 @@ function listTweetsViaBirdCommandEffect({
6277
6601
  maxPages
6278
6602
  }) {
6279
6603
  return Effect8.gen(function* () {
6280
- const args = [command, "-n", String(maxResults), "--json"];
6604
+ const args = [command, "-n", String(maxResults)];
6281
6605
  if (all) {
6282
6606
  args.push("--all");
6607
+ if (maxPages !== void 0) {
6608
+ args.push("--max-pages", String(maxPages));
6609
+ }
6283
6610
  }
6284
- if (maxPages !== void 0) {
6285
- args.push("--max-pages", String(maxPages));
6286
- }
6287
- const stdout = yield* runBirdJsonCommandEffect(args);
6611
+ const stdout = yield* runBirdTweetJsonCommandEffect(args);
6288
6612
  const payload = yield* parseBirdJsonEffect(stdout);
6289
6613
  return yield* normalizeBirdTweetsPayloadEffect(payload, command);
6290
6614
  });
@@ -6303,14 +6627,14 @@ function listBookmarkedTweetsViaBirdEffect(options) {
6303
6627
  }
6304
6628
  function searchTweetsViaBirdEffect(query, options) {
6305
6629
  return Effect8.gen(function* () {
6306
- const args = ["search", query, "-n", String(options.maxResults), "--json"];
6630
+ const args = ["search", query, "-n", String(options.maxResults)];
6307
6631
  if (options.all) {
6308
6632
  args.push("--all");
6309
6633
  }
6310
6634
  if (options.all && options.maxPages !== void 0) {
6311
6635
  args.push("--max-pages", String(options.maxPages));
6312
6636
  }
6313
- const stdout = yield* runBirdJsonCommandEffect(args);
6637
+ const stdout = yield* runBirdTweetJsonCommandEffect(args);
6314
6638
  const payload = yield* parseBirdJsonEffect(stdout);
6315
6639
  return yield* normalizeBirdTweetsPayloadEffect(payload, "search");
6316
6640
  });
@@ -6323,11 +6647,7 @@ function lookupTweetsByIdsViaBirdEffect(ids) {
6323
6647
  const tweets = yield* Effect8.forEach(
6324
6648
  ids,
6325
6649
  (id) => Effect8.gen(function* () {
6326
- const stdout = yield* runBirdJsonCommandEffect([
6327
- "read",
6328
- id,
6329
- "--json"
6330
- ]);
6650
+ const stdout = yield* runBirdTweetJsonCommandEffect(["read", id]);
6331
6651
  const payload = yield* parseBirdJsonEffect(stdout);
6332
6652
  return yield* normalizeBirdTweetItemEffect(payload, "read");
6333
6653
  }),
@@ -6341,11 +6661,11 @@ function listHomeTimelineViaBirdEffect({
6341
6661
  following = true
6342
6662
  }) {
6343
6663
  return Effect8.gen(function* () {
6344
- const args = ["home", "-n", String(maxResults), "--json"];
6664
+ const args = ["home", "-n", String(maxResults)];
6345
6665
  if (following) {
6346
6666
  args.push("--following");
6347
6667
  }
6348
- const stdout = yield* runBirdJsonCommandEffect(args);
6668
+ const stdout = yield* runBirdTweetJsonCommandEffect(args);
6349
6669
  const payload = yield* parseBirdJsonEffect(stdout);
6350
6670
  return yield* normalizeBirdTweetsPayloadEffect(payload, "home");
6351
6671
  });
@@ -6358,12 +6678,14 @@ function normalizeBirdFollowUsers(payload, command, maxResults) {
6358
6678
  }
6359
6679
  const data = users.map(toXurlMentionUser).filter((user) => Boolean(user));
6360
6680
  const nextToken = !Array.isArray(rawPayload) && typeof rawPayload.nextCursor === "string" ? rawPayload.nextCursor : null;
6681
+ const paginationKnownComplete = Array.isArray(rawPayload) ? data.length < maxResults : nextToken === null;
6361
6682
  return {
6362
6683
  data,
6363
6684
  meta: {
6364
6685
  result_count: data.length,
6365
6686
  page_count: data.length > 0 ? Math.max(1, Math.ceil(data.length / maxResults)) : 1,
6366
- next_token: nextToken
6687
+ next_token: nextToken,
6688
+ pagination_known_complete: paginationKnownComplete
6367
6689
  }
6368
6690
  };
6369
6691
  }
@@ -6400,6 +6722,73 @@ function listFollowUsersViaBirdEffect({
6400
6722
  );
6401
6723
  });
6402
6724
  }
6725
+ function normalizeBirdLists(payload) {
6726
+ if (!Array.isArray(payload)) {
6727
+ throw new Error("bird lists returned unexpected JSON");
6728
+ }
6729
+ const data = payload.map((value) => {
6730
+ const item = value;
6731
+ if (!item.id || !item.name) return null;
6732
+ return {
6733
+ id: item.id,
6734
+ name: item.name,
6735
+ ...item.description ? { description: item.description } : {},
6736
+ ...typeof item.memberCount === "number" ? { memberCount: item.memberCount } : {},
6737
+ ...typeof item.subscriberCount === "number" ? { followerCount: item.subscriberCount } : {},
6738
+ ...typeof item.isPrivate === "boolean" ? { isPrivate: item.isPrivate } : {},
6739
+ ...item.owner?.id ? { ownerId: item.owner.id } : {},
6740
+ ...item.owner?.username ? { ownerUsername: item.owner.username } : {},
6741
+ ...item.owner?.name ? { ownerName: item.owner.name } : {},
6742
+ ...item.createdAt ? { createdAt: item.createdAt } : {},
6743
+ raw: value
6744
+ };
6745
+ }).filter((item) => Boolean(item));
6746
+ return { data, meta: { result_count: data.length, next_token: null } };
6747
+ }
6748
+ function listOwnedXListsViaBirdEffect({
6749
+ maxResults
6750
+ }) {
6751
+ return Effect8.gen(function* () {
6752
+ const stdout = yield* runBirdJsonCommandEffect([
6753
+ "lists",
6754
+ "-n",
6755
+ String(maxResults),
6756
+ "--json"
6757
+ ]);
6758
+ const payload = yield* parseBirdJsonEffect(stdout);
6759
+ return yield* Effect8.try({
6760
+ try: () => normalizeBirdLists(payload),
6761
+ catch: (error) => error
6762
+ });
6763
+ });
6764
+ }
6765
+ function listXListMembersViaBirdEffect({
6766
+ listId,
6767
+ maxResults,
6768
+ maxPages = 1
6769
+ }) {
6770
+ return Effect8.gen(function* () {
6771
+ const args = ["list-members", listId, "-n", String(maxResults), "--json"];
6772
+ if (maxPages > 1) {
6773
+ args.push("--all", "--max-pages", String(maxPages));
6774
+ }
6775
+ const stdout = yield* runBirdJsonCommandEffect(args);
6776
+ const payload = yield* parseBirdJsonEffect(stdout);
6777
+ const normalized = yield* normalizeBirdFollowUsersEffect(
6778
+ payload,
6779
+ "list-members",
6780
+ maxResults
6781
+ );
6782
+ if (Array.isArray(payload)) {
6783
+ normalized.meta = {
6784
+ ...normalized.meta,
6785
+ pagination_known_complete: false,
6786
+ pagination_inferred_complete: normalized.data.length < maxResults
6787
+ };
6788
+ }
6789
+ return normalized;
6790
+ });
6791
+ }
6403
6792
  function listThreadViaBirdEffect({
6404
6793
  tweetId,
6405
6794
  all,
@@ -6407,14 +6796,14 @@ function listThreadViaBirdEffect({
6407
6796
  timeoutMs
6408
6797
  }) {
6409
6798
  return Effect8.gen(function* () {
6410
- const args = ["thread", tweetId, "--json"];
6799
+ const args = ["thread", tweetId];
6411
6800
  if (all) {
6412
6801
  args.push("--all");
6413
6802
  }
6414
6803
  if (maxPages !== void 0) {
6415
6804
  args.push("--max-pages", String(maxPages));
6416
6805
  }
6417
- const stdout = yield* runBirdJsonCommandEffect(args, timeoutMs);
6806
+ const stdout = yield* runBirdTweetJsonCommandEffect(args, timeoutMs);
6418
6807
  const payload = yield* parseBirdJsonEffect(stdout);
6419
6808
  return yield* normalizeBirdTweetsPayloadEffect(payload, "thread");
6420
6809
  });
@@ -6447,9 +6836,11 @@ function parseBirdWhoami(stdout) {
6447
6836
  }
6448
6837
  return line.match(/^\D*(\d{2,})\D*$/)?.[1];
6449
6838
  }).find((value) => Boolean(value));
6839
+ const name = stdout.match(/@[A-Za-z0-9_]{1,15}\s*\(([^)]+)\)/)?.[1]?.trim();
6450
6840
  return {
6451
6841
  username: usernameMatch[1],
6452
- ...id ? { id } : {}
6842
+ ...id ? { id } : {},
6843
+ ...name ? { name } : {}
6453
6844
  };
6454
6845
  }
6455
6846
  function getAuthenticatedBirdAccountEffect() {
@@ -6461,6 +6852,9 @@ function getAuthenticatedBirdAccountEffect() {
6461
6852
  });
6462
6853
  });
6463
6854
  }
6855
+ function getAuthenticatedBirdAccount() {
6856
+ return runEffectPromise(getAuthenticatedBirdAccountEffect());
6857
+ }
6464
6858
  function listDirectMessagesViaBirdEffect({
6465
6859
  maxResults,
6466
6860
  inbox = "all",
@@ -7104,6 +7498,71 @@ function lookupUsersByIdsEffect(ids) {
7104
7498
  function lookupUsersByIds(ids) {
7105
7499
  return runEffectPromise(lookupUsersByIdsEffect(ids));
7106
7500
  }
7501
+ function normalizeXListPage(payload) {
7502
+ const rawItems = Array.isArray(payload.data) ? payload.data : [];
7503
+ const data = rawItems.map((value) => {
7504
+ const item = value;
7505
+ if (typeof item.id !== "string" || typeof item.name !== "string") {
7506
+ return null;
7507
+ }
7508
+ return {
7509
+ id: item.id,
7510
+ name: item.name,
7511
+ ...typeof item.description === "string" ? { description: item.description } : {},
7512
+ ...typeof item.member_count === "number" ? { memberCount: item.member_count } : {},
7513
+ ...typeof item.follower_count === "number" ? { followerCount: item.follower_count } : {},
7514
+ ...typeof item.private === "boolean" ? { isPrivate: item.private } : {},
7515
+ ...typeof item.owner_id === "string" ? { ownerId: item.owner_id } : {},
7516
+ ...typeof item.created_at === "string" ? { createdAt: item.created_at } : {},
7517
+ raw: item
7518
+ };
7519
+ }).filter((item) => Boolean(item));
7520
+ const meta = payload.meta && typeof payload.meta === "object" ? payload.meta : {};
7521
+ return {
7522
+ data,
7523
+ meta: {
7524
+ result_count: data.length,
7525
+ ...typeof meta.next_token === "string" ? { next_token: meta.next_token } : { next_token: null }
7526
+ }
7527
+ };
7528
+ }
7529
+ function listOwnedXListsViaXurlEffect({
7530
+ userId,
7531
+ username,
7532
+ maxResults,
7533
+ paginationToken
7534
+ }) {
7535
+ const query = new URLSearchParams({
7536
+ max_results: String(maxResults),
7537
+ "list.fields": "created_at,description,follower_count,member_count,name,owner_id,private"
7538
+ });
7539
+ if (paginationToken) query.set("pagination_token", paginationToken);
7540
+ return runOAuth2JsonCommandEffect({
7541
+ args: [`/2/users/${userId}/owned_lists?${query.toString()}`],
7542
+ username
7543
+ }).pipe(Effect9.map(normalizeXListPage));
7544
+ }
7545
+ function listXListMembersViaXurlEffect({
7546
+ listId,
7547
+ username,
7548
+ maxResults,
7549
+ paginationToken
7550
+ }) {
7551
+ const query = new URLSearchParams({
7552
+ max_results: String(maxResults),
7553
+ "user.fields": RICH_USER_FIELDS
7554
+ });
7555
+ if (paginationToken) query.set("pagination_token", paginationToken);
7556
+ return runOAuth2JsonCommandEffect({
7557
+ args: [`/2/lists/${listId}/members?${query.toString()}`],
7558
+ username
7559
+ }).pipe(
7560
+ Effect9.map((payload) => ({
7561
+ data: Array.isArray(payload.data) ? payload.data : [],
7562
+ meta: payload.meta && typeof payload.meta === "object" ? payload.meta : {}
7563
+ }))
7564
+ );
7565
+ }
7107
7566
  function lookupUsersByHandlesEffect(handles, options = {}) {
7108
7567
  if (handles.length === 0) {
7109
7568
  return Effect9.succeed([]);
@@ -9660,6 +10119,16 @@ function ensureStubProfileForXUser(db, externalUserId) {
9660
10119
  function getReferencedTweetId(tweet, type) {
9661
10120
  return tweet.referenced_tweets?.find((item) => item.type === type)?.id ?? null;
9662
10121
  }
10122
+ function toCanonicalTweets(payload) {
10123
+ const tweetsById = /* @__PURE__ */ new Map();
10124
+ for (const tweet of payload.includes?.tweets ?? []) {
10125
+ tweetsById.set(tweet.id, tweet);
10126
+ }
10127
+ for (const tweet of payload.data) {
10128
+ tweetsById.set(tweet.id, tweet);
10129
+ }
10130
+ return tweetsById.values();
10131
+ }
9663
10132
  function replaceTweetFts(db, tweetId, text) {
9664
10133
  db.prepare("delete from tweets_fts where tweet_id = ?").run(tweetId);
9665
10134
  db.prepare("insert into tweets_fts (tweet_id, text) values (?, ?)").run(
@@ -9689,7 +10158,7 @@ function ingestTweetPayload(db, {
9689
10158
  created_at = excluded.created_at,
9690
10159
  is_replied = max(tweets.is_replied, excluded.is_replied),
9691
10160
  reply_to_id = coalesce(tweets.reply_to_id, excluded.reply_to_id),
9692
- like_count = excluded.like_count,
10161
+ like_count = case when ? then tweets.like_count else excluded.like_count end,
9693
10162
  media_count = max(tweets.media_count, excluded.media_count),
9694
10163
  entities_json = excluded.entities_json,
9695
10164
  media_json = case
@@ -9710,25 +10179,29 @@ function ingestTweetPayload(db, {
9710
10179
  const tweetIds = [];
9711
10180
  db.transaction(() => {
9712
10181
  const observedAt = (/* @__PURE__ */ new Date()).toISOString();
9713
- for (const tweet of payload.data) {
10182
+ const primaryTweetIds = new Set(payload.data.map((tweet) => tweet.id));
10183
+ for (const tweet of toCanonicalTweets(payload)) {
10184
+ const isPrimaryTweet = primaryTweetIds.has(tweet.id);
9714
10185
  const author = usersById.get(tweet.author_id);
9715
10186
  const profile = author ? upsertProfileFromXUser(db, author) : ensureStubProfileForXUser(db, tweet.author_id);
9716
10187
  const replyToId = getReferencedTweetId(tweet, "replied_to");
9717
10188
  const quotedTweetId = getReferencedTweetId(tweet, "quoted");
10189
+ const shouldMarkReplied = isPrimaryTweet && markRepliesAsReplied && Boolean(replyToId);
9718
10190
  upsertTweet.run(
9719
10191
  tweet.id,
9720
10192
  profile.profile.id,
9721
10193
  tweet.text,
9722
10194
  tweet.created_at,
9723
- markRepliesAsReplied && replyToId ? 1 : 0,
10195
+ shouldMarkReplied ? 1 : 0,
9724
10196
  replyToId,
9725
10197
  Number(tweet.public_metrics?.like_count ?? 0),
9726
10198
  countTweetMedia(tweet),
9727
10199
  JSON.stringify(tweetEntitiesFromXurl(tweet.entities)),
9728
10200
  buildMediaJsonFromIncludes(tweet, payload.includes?.media),
9729
- quotedTweetId
10201
+ quotedTweetId,
10202
+ !isPrimaryTweet && tweet.public_metrics?.like_count === void 0 ? 1 : 0
9730
10203
  );
9731
- if (edgeKind) {
10204
+ if (edgeKind && isPrimaryTweet) {
9732
10205
  upsertTweetAccountEdge(db, {
9733
10206
  accountId,
9734
10207
  tweetId: tweet.id,
@@ -9738,16 +10211,20 @@ function ingestTweetPayload(db, {
9738
10211
  rawJson: JSON.stringify(tweet)
9739
10212
  });
9740
10213
  }
9741
- upsertCollection?.run(
9742
- accountId,
9743
- tweet.id,
9744
- collectionKind2,
9745
- source,
9746
- JSON.stringify(tweet),
9747
- observedAt
9748
- );
10214
+ if (isPrimaryTweet) {
10215
+ upsertCollection?.run(
10216
+ accountId,
10217
+ tweet.id,
10218
+ collectionKind2,
10219
+ source,
10220
+ JSON.stringify(tweet),
10221
+ observedAt
10222
+ );
10223
+ }
9749
10224
  replaceTweetFts(db, tweet.id, tweet.text);
9750
- tweetIds.push(tweet.id);
10225
+ if (isPrimaryTweet) {
10226
+ tweetIds.push(tweet.id);
10227
+ }
9751
10228
  }
9752
10229
  })();
9753
10230
  return tweetIds;
@@ -10719,6 +11196,8 @@ var RECENT_TIMELINE_EDGE_CANDIDATES = 5e3;
10719
11196
  function listTimelineItems({
10720
11197
  resource,
10721
11198
  account,
11199
+ listAccountId,
11200
+ listId,
10722
11201
  search,
10723
11202
  replyFilter = "all",
10724
11203
  since,
@@ -10749,7 +11228,7 @@ function listTimelineItems({
10749
11228
  let join3 = "";
10750
11229
  let where = "where e.kind = ?";
10751
11230
  let searchSnippetSelect = "";
10752
- const canUseRecentEdgeWindow = !likedOnly && !bookmarkedOnly && !account && !search?.trim() && replyFilter === "all" && !since?.trim() && !until?.trim() && includeReplies && qualityFilter === "all";
11231
+ const canUseRecentEdgeWindow = !likedOnly && !bookmarkedOnly && !listId && !account && !search?.trim() && replyFilter === "all" && !since?.trim() && !until?.trim() && includeReplies && qualityFilter === "all";
10753
11232
  if (likedOnly || bookmarkedOnly) {
10754
11233
  if (likedOnly && bookmarkedOnly) {
10755
11234
  timelineEdgesCte = `
@@ -10845,6 +11324,22 @@ function listTimelineItems({
10845
11324
  params.push(until.trim());
10846
11325
  }
10847
11326
  }
11327
+ if (Boolean(listId) !== Boolean(listAccountId)) {
11328
+ throw new Error("List filtering requires both listId and listAccountId");
11329
+ }
11330
+ if (listId && listAccountId) {
11331
+ where += `
11332
+ and exists (
11333
+ select 1
11334
+ from x_list_members list_member
11335
+ where list_member.account_id = ?
11336
+ and list_member.list_id = ?
11337
+ and list_member.profile_id = t.author_profile_id
11338
+ and list_member.current = 1
11339
+ )
11340
+ `;
11341
+ params.push(listAccountId, listId);
11342
+ }
10848
11343
  const ftsSearch = search?.trim() ? toFtsSearchQuery(search) : "";
10849
11344
  if (ftsSearch) {
10850
11345
  join3 += " join tweets_fts on tweets_fts.tweet_id = t.id ";
@@ -12518,6 +13013,7 @@ function reachedStartTimeBoundary(payload, startTimeMs) {
12518
13013
  function mergeTimelinePayloads(payloads, limit) {
12519
13014
  const data = [];
12520
13015
  const usersById = /* @__PURE__ */ new Map();
13016
+ const tweetsById = /* @__PURE__ */ new Map();
12521
13017
  const mediaByKey = /* @__PURE__ */ new Map();
12522
13018
  let meta;
12523
13019
  for (const payload of payloads) {
@@ -12530,6 +13026,9 @@ function mergeTimelinePayloads(payloads, limit) {
12530
13026
  for (const user of payload.includes?.users ?? []) {
12531
13027
  usersById.set(user.id, user);
12532
13028
  }
13029
+ for (const tweet of payload.includes?.tweets ?? []) {
13030
+ tweetsById.set(tweet.id, tweet);
13031
+ }
12533
13032
  for (const media of payload.includes?.media ?? []) {
12534
13033
  mediaByKey.set(media.media_key, media);
12535
13034
  }
@@ -12539,6 +13038,7 @@ function mergeTimelinePayloads(payloads, limit) {
12539
13038
  data,
12540
13039
  includes: {
12541
13040
  users: [...usersById.values()],
13041
+ tweets: [...tweetsById.values()],
12542
13042
  media: [...mediaByKey.values()]
12543
13043
  },
12544
13044
  meta
@@ -12847,6 +13347,7 @@ function compactTweet(source, item) {
12847
13347
  createdAt: item.createdAt,
12848
13348
  text: item.text,
12849
13349
  entities: item.entities,
13350
+ media: item.media,
12850
13351
  likeCount: item.likeCount,
12851
13352
  liked: item.liked,
12852
13353
  bookmarked: item.bookmarked,
@@ -12866,6 +13367,7 @@ function compactEmbeddedTweet(item) {
12866
13367
  createdAt: item.createdAt,
12867
13368
  text: item.text,
12868
13369
  entities: item.entities,
13370
+ media: item.media,
12869
13371
  likeCount: item.likeCount ?? 0,
12870
13372
  liked: Boolean(item.liked),
12871
13373
  bookmarked: Boolean(item.bookmarked),
@@ -15390,6 +15892,8 @@ function compactTweet2(source, item) {
15390
15892
  authorProfile: item.author,
15391
15893
  createdAt: item.createdAt,
15392
15894
  text: item.text,
15895
+ entities: item.entities,
15896
+ media: item.media,
15393
15897
  likeCount: item.likeCount,
15394
15898
  liked: item.liked,
15395
15899
  bookmarked: item.bookmarked,
@@ -15427,7 +15931,9 @@ function collectLiveSearchTweets(options) {
15427
15931
  t.created_at,
15428
15932
  t.is_replied,
15429
15933
  t.like_count,
15430
- t.media_count,
15934
+ t.media_count,
15935
+ t.entities_json,
15936
+ t.media_json,
15431
15937
  case
15432
15938
  when exists (
15433
15939
  select 1 from tweet_collections collection
@@ -15481,6 +15987,8 @@ function collectLiveSearchTweets(options) {
15481
15987
  },
15482
15988
  createdAt: String(row.created_at),
15483
15989
  text: String(row.text),
15990
+ entities: parseJsonField2(row.entities_json, {}),
15991
+ media: parseJsonField2(row.media_json, []),
15484
15992
  likeCount: Number(row.like_count),
15485
15993
  liked: Boolean(row.liked),
15486
15994
  bookmarked: Boolean(row.bookmarked),
@@ -18837,14 +19345,77 @@ function trySync7(try_) {
18837
19345
  catch: toError11
18838
19346
  });
18839
19347
  }
19348
+ var SEEDED_ACCOUNT_HANDLE = "steipete";
19349
+ var SEEDED_ACCOUNT_EXTERNAL_USER_ID = "25401953";
19350
+ function hydrateAccountFromBirdEffect() {
19351
+ return Effect34.gen(function* () {
19352
+ const account = yield* tryPromise(() => getAuthenticatedBirdAccount()).pipe(
19353
+ Effect34.catchAll(() => Effect34.succeed(null))
19354
+ );
19355
+ if (!account?.username) return false;
19356
+ const handle = account.username.replace(/^@/, "");
19357
+ const name = account.name?.trim() || null;
19358
+ const externalUserId = account.id ?? null;
19359
+ const hydrated = yield* trySync7(() => {
19360
+ const db = getNativeDb();
19361
+ return db.transaction(() => {
19362
+ const current = db.prepare(
19363
+ `select handle, external_user_id, transport from accounts where id = 'acct_primary'`
19364
+ ).get();
19365
+ if (!current) return false;
19366
+ const storedHandle = current?.handle?.replace(/^@/, "") ?? null;
19367
+ const handleMatches = storedHandle?.toLowerCase() === handle.toLowerCase();
19368
+ const identityMatches = externalUserId !== null && current.external_user_id !== null ? externalUserId === current.external_user_id : handleMatches;
19369
+ const isSeededPlaceholder = storedHandle?.toLowerCase() === SEEDED_ACCOUNT_HANDLE && current.external_user_id === SEEDED_ACCOUNT_EXTERNAL_USER_ID && current.transport === "xurl";
19370
+ if (!identityMatches && !isSeededPlaceholder) return false;
19371
+ const identityChanged = !identityMatches;
19372
+ if (identityChanged) {
19373
+ db.prepare(
19374
+ `update profiles
19375
+ set handle = ?,
19376
+ display_name = coalesce(?, display_name),
19377
+ avatar_url = null
19378
+ where id = 'profile_me'`
19379
+ ).run(handle, name);
19380
+ db.prepare(
19381
+ `update accounts
19382
+ set handle = ?,
19383
+ name = coalesce(?, name),
19384
+ transport = 'bird',
19385
+ external_user_id = ?
19386
+ where id = 'acct_primary'`
19387
+ ).run(`@${handle}`, name, externalUserId);
19388
+ } else {
19389
+ db.prepare(
19390
+ `update profiles
19391
+ set handle = ?,
19392
+ display_name = coalesce(?, display_name)
19393
+ where id = 'profile_me'`
19394
+ ).run(handle, name);
19395
+ db.prepare(
19396
+ `update accounts
19397
+ set handle = ?,
19398
+ name = coalesce(?, name),
19399
+ transport = 'bird',
19400
+ external_user_id = coalesce(?, external_user_id)
19401
+ where id = 'acct_primary'`
19402
+ ).run(`@${handle}`, name, externalUserId);
19403
+ }
19404
+ return true;
19405
+ })();
19406
+ });
19407
+ return hydrated;
19408
+ });
19409
+ }
18840
19410
  function hydrateProfilesFromXEffect() {
18841
19411
  return Effect34.gen(function* () {
18842
19412
  const transport = yield* tryPromise(() => getTransportStatus());
18843
19413
  if (transport.availableTransport !== "xurl") {
19414
+ const hydratedAccount2 = yield* hydrateAccountFromBirdEffect();
18844
19415
  return {
18845
19416
  ok: true,
18846
19417
  hydratedProfiles: 0,
18847
- hydratedAccount: false,
19418
+ hydratedAccount: hydratedAccount2,
18848
19419
  reason: transport.statusText
18849
19420
  };
18850
19421
  }
@@ -21918,6 +22489,7 @@ import { Effect as Effect42 } from "effect";
21918
22489
 
21919
22490
  // src/lib/openai.ts
21920
22491
  import { Effect as Effect41 } from "effect";
22492
+ var getEnv = (name) => process.env[name];
21921
22493
  function clampScore(value) {
21922
22494
  return Math.max(0, Math.min(100, Math.round(value)));
21923
22495
  }
@@ -21937,8 +22509,11 @@ function scoreInboxItemWithOpenAIEffect(input) {
21937
22509
  return yield* Effect41.fail(new Error("OPENAI_API_KEY is not set"));
21938
22510
  }
21939
22511
  const model = process.env.BIRDCLAW_OPENAI_MODEL || "gpt-5.2";
22512
+ const baseUrl = resolveOpenAIBaseUrl(getEnv);
22513
+ const url = `${baseUrl}/chat/completions`;
22514
+ debugLog(getEnv, `POST ${url} (model=${model})`);
21940
22515
  const response = yield* tryPromise(
21941
- () => fetch("https://api.openai.com/v1/chat/completions", {
22516
+ () => fetch(url, {
21942
22517
  method: "POST",
21943
22518
  headers: {
21944
22519
  authorization: `Bearer ${apiKey}`,
@@ -21959,10 +22534,23 @@ function scoreInboxItemWithOpenAIEffect(input) {
21959
22534
  ]
21960
22535
  })
21961
22536
  })
21962
- ).pipe(Effect41.mapError(toError15));
22537
+ ).pipe(
22538
+ Effect41.mapError(toError15),
22539
+ Effect41.tapError(
22540
+ (error) => Effect41.sync(
22541
+ () => debugLog(getEnv, `network error for ${url}: ${error.message}`)
22542
+ )
22543
+ )
22544
+ );
21963
22545
  if (!response.ok) {
22546
+ const text = yield* tryPromise(() => response.text()).pipe(
22547
+ Effect41.mapError(toError15)
22548
+ );
22549
+ debugLog(getEnv, `${url} -> ${response.status} ${text.slice(0, 400)}`);
21964
22550
  return yield* Effect41.fail(
21965
- new Error(`OpenAI request failed: ${response.status}`)
22551
+ new Error(
22552
+ `OpenAI request failed: ${response.status} ${text.slice(0, 400)}`
22553
+ )
21966
22554
  );
21967
22555
  }
21968
22556
  const payload = yield* tryPromise(() => response.json()).pipe(
@@ -23371,19 +23959,550 @@ function registerJobCommands({ program: program2, print: print2 }) {
23371
23959
  });
23372
23960
  }
23373
23961
 
23962
+ // src/lib/x-lists.ts
23963
+ import { Effect as Effect47 } from "effect";
23964
+ var DEFAULT_MAX_LISTS = 20;
23965
+ var DEFAULT_MEMBER_LIMIT = 20;
23966
+ var DEFAULT_MAX_MEMBER_PAGES = 1;
23967
+ var DEFAULT_DELAY_MS2 = 1e3;
23968
+ var MAX_LISTS = 100;
23969
+ var MAX_MEMBER_LIMIT = 100;
23970
+ var MAX_MEMBER_PAGES = 100;
23971
+ function positiveInteger(name, value, maximum) {
23972
+ if (!Number.isFinite(value) || value < 1 || value > maximum) {
23973
+ throw new Error(`${name} must be between 1 and ${String(maximum)}`);
23974
+ }
23975
+ return Math.floor(value);
23976
+ }
23977
+ function nonNegativeInteger(name, value) {
23978
+ if (!Number.isFinite(value) || value < 0) {
23979
+ throw new Error(`${name} must be a non-negative integer`);
23980
+ }
23981
+ return Math.floor(value);
23982
+ }
23983
+ function normalizedMode(value) {
23984
+ if (value === void 0) return "auto";
23985
+ if (value === "auto" || value === "bird" || value === "xurl") return value;
23986
+ throw new Error("--mode must be auto, bird, or xurl");
23987
+ }
23988
+ function metaString(meta, key) {
23989
+ const value = meta?.[key];
23990
+ return typeof value === "string" && value.length > 0 ? value : void 0;
23991
+ }
23992
+ function metaNumber(meta, key) {
23993
+ const value = meta?.[key];
23994
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
23995
+ }
23996
+ function parseJsonRecord(value) {
23997
+ if (typeof value !== "string" || value.length === 0) return {};
23998
+ try {
23999
+ const parsed = JSON.parse(value);
24000
+ return parsed && typeof parsed === "object" ? parsed : {};
24001
+ } catch {
24002
+ return {};
24003
+ }
24004
+ }
24005
+ function fetchOwnedListsEffect({
24006
+ source,
24007
+ account,
24008
+ maxLists
24009
+ }) {
24010
+ if (source === "bird") {
24011
+ return Effect47.gen(function* () {
24012
+ const authenticated = yield* getAuthenticatedBirdAccountEffect();
24013
+ yield* trySync(
24014
+ () => assertLiveAccountMatches({
24015
+ source: "bird",
24016
+ account,
24017
+ liveUsername: authenticated.username,
24018
+ liveExternalUserId: authenticated.id
24019
+ })
24020
+ );
24021
+ return yield* listOwnedXListsViaBirdEffect({ maxResults: maxLists });
24022
+ });
24023
+ }
24024
+ return Effect47.gen(function* () {
24025
+ const authenticated = yield* lookupAuthenticatedOAuth2UserEffect(
24026
+ account.username
24027
+ );
24028
+ if (!authenticated) {
24029
+ return yield* Effect47.fail(
24030
+ new Error("xurl did not return an OAuth2 user")
24031
+ );
24032
+ }
24033
+ const liveUsername = String(authenticated.username ?? "");
24034
+ const liveExternalUserId = String(authenticated.id ?? "");
24035
+ yield* trySync(
24036
+ () => assertLiveAccountMatches({
24037
+ source: "xurl",
24038
+ account,
24039
+ liveUsername,
24040
+ liveExternalUserId
24041
+ })
24042
+ );
24043
+ const userId = account.externalUserId ?? liveExternalUserId;
24044
+ if (!userId) {
24045
+ return yield* Effect47.fail(
24046
+ new Error("xurl authenticated user is missing an id")
24047
+ );
24048
+ }
24049
+ return yield* listOwnedXListsViaXurlEffect({
24050
+ userId,
24051
+ username: account.username,
24052
+ maxResults: maxLists
24053
+ });
24054
+ });
24055
+ }
24056
+ function fetchListMembersEffect({
24057
+ source,
24058
+ list,
24059
+ username,
24060
+ memberLimit,
24061
+ maxMemberPages
24062
+ }) {
24063
+ if (source === "bird") {
24064
+ return listXListMembersViaBirdEffect({
24065
+ listId: list.id,
24066
+ maxResults: memberLimit,
24067
+ maxPages: maxMemberPages
24068
+ });
24069
+ }
24070
+ return Effect47.gen(function* () {
24071
+ const data = [];
24072
+ const seen = /* @__PURE__ */ new Set();
24073
+ let paginationToken;
24074
+ let pageCount = 0;
24075
+ do {
24076
+ const page = yield* listXListMembersViaXurlEffect({
24077
+ listId: list.id,
24078
+ username,
24079
+ maxResults: memberLimit,
24080
+ paginationToken
24081
+ });
24082
+ pageCount += 1;
24083
+ for (const user of page.data) {
24084
+ if (seen.has(user.id)) continue;
24085
+ seen.add(user.id);
24086
+ data.push(user);
24087
+ }
24088
+ paginationToken = metaString(page.meta, "next_token");
24089
+ } while (paginationToken && pageCount < maxMemberPages);
24090
+ return {
24091
+ data,
24092
+ meta: {
24093
+ result_count: data.length,
24094
+ page_count: pageCount,
24095
+ next_token: paginationToken ?? null,
24096
+ pagination_known_complete: !paginationToken
24097
+ }
24098
+ };
24099
+ });
24100
+ }
24101
+ function membershipStatus({
24102
+ list,
24103
+ payload
24104
+ }) {
24105
+ if (payload.meta?.pagination_known_complete === true) return "complete";
24106
+ if (payload.meta?.pagination_inferred_complete === true) return "inferred";
24107
+ if (typeof list.memberCount === "number" && payload.data.length >= list.memberCount) {
24108
+ return "inferred";
24109
+ }
24110
+ return "partial";
24111
+ }
24112
+ function ownerProfileId(db, list) {
24113
+ if (!list.ownerId || !list.ownerUsername) return void 0;
24114
+ return upsertProfileFromXUser(db, {
24115
+ id: list.ownerId,
24116
+ username: list.ownerUsername,
24117
+ name: list.ownerName ?? list.ownerUsername
24118
+ }).profile.id;
24119
+ }
24120
+ function persistListResult({
24121
+ db,
24122
+ accountId,
24123
+ list,
24124
+ source,
24125
+ payload,
24126
+ status,
24127
+ now: now2,
24128
+ rateLimit
24129
+ }) {
24130
+ return db.transaction(() => {
24131
+ const listOwnerProfileId = ownerProfileId(db, list);
24132
+ const membersSyncedAt = payload ? now2 : null;
24133
+ const pageCount = payload ? metaNumber(payload.meta, "page_count") ?? 1 : 0;
24134
+ const resultCount = payload?.data.length ?? 0;
24135
+ db.prepare(
24136
+ `
24137
+ insert into x_lists (
24138
+ account_id, list_id, name, description, owner_profile_id,
24139
+ owner_external_user_id, is_private, member_count, follower_count,
24140
+ source, membership_status, lists_synced_at, members_synced_at,
24141
+ member_page_count, member_result_count, rate_limit_json, raw_json,
24142
+ updated_at
24143
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
24144
+ on conflict(account_id, list_id) do update set
24145
+ name = excluded.name,
24146
+ description = excluded.description,
24147
+ owner_profile_id = coalesce(excluded.owner_profile_id, x_lists.owner_profile_id),
24148
+ owner_external_user_id = coalesce(excluded.owner_external_user_id, x_lists.owner_external_user_id),
24149
+ is_private = excluded.is_private,
24150
+ member_count = coalesce(excluded.member_count, x_lists.member_count),
24151
+ follower_count = coalesce(excluded.follower_count, x_lists.follower_count),
24152
+ source = excluded.source,
24153
+ membership_status = excluded.membership_status,
24154
+ lists_synced_at = excluded.lists_synced_at,
24155
+ members_synced_at = coalesce(excluded.members_synced_at, x_lists.members_synced_at),
24156
+ member_page_count = excluded.member_page_count,
24157
+ member_result_count = excluded.member_result_count,
24158
+ rate_limit_json = excluded.rate_limit_json,
24159
+ raw_json = excluded.raw_json,
24160
+ updated_at = excluded.updated_at
24161
+ `
24162
+ ).run(
24163
+ accountId,
24164
+ list.id,
24165
+ list.name,
24166
+ list.description ?? "",
24167
+ listOwnerProfileId ?? null,
24168
+ list.ownerId ?? null,
24169
+ list.isPrivate ? 1 : 0,
24170
+ list.memberCount ?? null,
24171
+ list.followerCount ?? null,
24172
+ source,
24173
+ status,
24174
+ now2,
24175
+ membersSyncedAt,
24176
+ pageCount,
24177
+ resultCount,
24178
+ JSON.stringify(rateLimit),
24179
+ JSON.stringify(list.raw ?? {}),
24180
+ now2
24181
+ );
24182
+ if (!payload) return;
24183
+ if (status === "complete") {
24184
+ db.prepare(
24185
+ `
24186
+ update x_list_members
24187
+ set current = 0, ended_at = ?, updated_at = ?
24188
+ where account_id = ? and list_id = ? and current = 1
24189
+ `
24190
+ ).run(now2, now2, accountId, list.id);
24191
+ }
24192
+ const upsertMember = db.prepare(`
24193
+ insert into x_list_members (
24194
+ account_id, list_id, profile_id, external_user_id, source, current,
24195
+ first_seen_at, last_seen_at, ended_at, raw_json, updated_at
24196
+ ) values (?, ?, ?, ?, ?, 1, ?, ?, null, ?, ?)
24197
+ on conflict(account_id, list_id, profile_id) do update set
24198
+ external_user_id = excluded.external_user_id,
24199
+ source = excluded.source,
24200
+ current = 1,
24201
+ last_seen_at = excluded.last_seen_at,
24202
+ ended_at = null,
24203
+ raw_json = excluded.raw_json,
24204
+ updated_at = excluded.updated_at
24205
+ `);
24206
+ for (const user of payload.data) {
24207
+ const resolved = upsertProfileFromXUser(db, user);
24208
+ upsertMember.run(
24209
+ accountId,
24210
+ list.id,
24211
+ resolved.profile.id,
24212
+ resolved.externalUserId,
24213
+ source,
24214
+ now2,
24215
+ now2,
24216
+ JSON.stringify(user),
24217
+ now2
24218
+ );
24219
+ }
24220
+ })();
24221
+ }
24222
+ function syncXListsEffect(options = {}) {
24223
+ return Effect47.suspend(() => {
24224
+ const mode = normalizedMode(options.mode);
24225
+ const maxLists = positiveInteger(
24226
+ "--max-lists",
24227
+ options.maxLists ?? DEFAULT_MAX_LISTS,
24228
+ MAX_LISTS
24229
+ );
24230
+ const memberLimit = positiveInteger(
24231
+ "--member-limit",
24232
+ options.memberLimit ?? DEFAULT_MEMBER_LIMIT,
24233
+ MAX_MEMBER_LIMIT
24234
+ );
24235
+ const maxMemberPages = positiveInteger(
24236
+ "--max-member-pages",
24237
+ options.maxMemberPages ?? DEFAULT_MAX_MEMBER_PAGES,
24238
+ MAX_MEMBER_PAGES
24239
+ );
24240
+ const delayMs = nonNegativeInteger(
24241
+ "--delay-ms",
24242
+ options.delayMs ?? DEFAULT_DELAY_MS2
24243
+ );
24244
+ const db = getNativeDb();
24245
+ const account = resolveLiveSyncAccount(db, options.account);
24246
+ const sources = mode === "auto" ? ["bird", "xurl"] : [mode];
24247
+ const transports = sources.map(
24248
+ (source) => createLiveTransportAdapter(
24249
+ source,
24250
+ fetchOwnedListsEffect({ source, account, maxLists })
24251
+ )
24252
+ );
24253
+ return Effect47.gen(function* () {
24254
+ const discovered = yield* fetchWithTransportFallbackEffect(transports);
24255
+ const source = discovered.source;
24256
+ const lists = discovered.payload.data.slice(0, maxLists);
24257
+ const results = [];
24258
+ for (const [index, list] of lists.entries()) {
24259
+ if (index > 0 && delayMs > 0) yield* Effect47.sleep(delayMs);
24260
+ const fetched = yield* fetchListMembersEffect({
24261
+ source,
24262
+ list,
24263
+ username: account.username,
24264
+ memberLimit,
24265
+ maxMemberPages
24266
+ }).pipe(
24267
+ Effect47.map((payload) => ({ ok: true, payload })),
24268
+ Effect47.catchAll(
24269
+ (error) => Effect47.succeed({ ok: false, error: toError(error) })
24270
+ )
24271
+ );
24272
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
24273
+ const rateLimit = {
24274
+ memberLimit,
24275
+ maxMemberPages,
24276
+ delayMs,
24277
+ nextCursorStored: fetched.ok ? Boolean(metaString(fetched.payload.meta, "next_token")) : false
24278
+ };
24279
+ if (!fetched.ok) {
24280
+ yield* databaseWriteEffect(
24281
+ (writeDb) => persistListResult({
24282
+ db: writeDb,
24283
+ accountId: account.accountId,
24284
+ list,
24285
+ source,
24286
+ status: "error",
24287
+ now: now2,
24288
+ rateLimit
24289
+ })
24290
+ );
24291
+ results.push({
24292
+ listId: list.id,
24293
+ name: list.name,
24294
+ status: "error",
24295
+ error: fetched.error.message
24296
+ });
24297
+ continue;
24298
+ }
24299
+ const status = membershipStatus({ list, payload: fetched.payload });
24300
+ yield* databaseWriteEffect(
24301
+ (writeDb) => persistListResult({
24302
+ db: writeDb,
24303
+ accountId: account.accountId,
24304
+ list,
24305
+ source,
24306
+ payload: fetched.payload,
24307
+ status,
24308
+ now: now2,
24309
+ rateLimit
24310
+ })
24311
+ );
24312
+ results.push({
24313
+ listId: list.id,
24314
+ name: list.name,
24315
+ status,
24316
+ members: fetched.payload.data.length,
24317
+ pages: metaNumber(fetched.payload.meta, "page_count") ?? 1
24318
+ });
24319
+ }
24320
+ const errors = results.filter((result) => result.status === "error");
24321
+ return {
24322
+ ok: errors.length === 0,
24323
+ accountId: account.accountId,
24324
+ mode,
24325
+ source,
24326
+ listCount: lists.length,
24327
+ membershipCompleteCount: results.filter(
24328
+ (result) => result.status === "complete"
24329
+ ).length,
24330
+ membershipPartialCount: results.filter(
24331
+ (result) => result.status === "partial"
24332
+ ).length,
24333
+ membershipInferredCount: results.filter(
24334
+ (result) => result.status === "inferred"
24335
+ ).length,
24336
+ errorCount: errors.length,
24337
+ rateLimit: { maxLists, memberLimit, maxMemberPages, delayMs },
24338
+ lists: results
24339
+ };
24340
+ });
24341
+ });
24342
+ }
24343
+ function syncXLists(options = {}) {
24344
+ return runEffectPromise(syncXListsEffect(options));
24345
+ }
24346
+ function accountIdForRead(db, account) {
24347
+ return resolveLiveSyncAccount(db, account).accountId;
24348
+ }
24349
+ function storedListFromRow(row) {
24350
+ return {
24351
+ accountId: String(row.account_id),
24352
+ listId: String(row.list_id),
24353
+ name: String(row.name),
24354
+ description: String(row.description ?? ""),
24355
+ ...row.owner_profile_id ? { ownerProfileId: String(row.owner_profile_id) } : {},
24356
+ ...row.owner_external_user_id ? { ownerExternalUserId: String(row.owner_external_user_id) } : {},
24357
+ isPrivate: Number(row.is_private) === 1,
24358
+ ...typeof row.member_count === "number" ? { memberCount: row.member_count } : {},
24359
+ ...typeof row.follower_count === "number" ? { followerCount: row.follower_count } : {},
24360
+ source: String(row.source),
24361
+ membershipStatus: String(row.membership_status),
24362
+ listsSyncedAt: String(row.lists_synced_at),
24363
+ ...row.members_synced_at ? { membersSyncedAt: String(row.members_synced_at) } : {},
24364
+ memberPageCount: Number(row.member_page_count),
24365
+ memberResultCount: Number(row.member_result_count),
24366
+ rateLimit: parseJsonRecord(row.rate_limit_json)
24367
+ };
24368
+ }
24369
+ function listStoredXLists(options = {}) {
24370
+ const db = getReadDb();
24371
+ const accountId = accountIdForRead(db, options.account);
24372
+ return db.prepare(
24373
+ `select * from x_lists where account_id = ? order by name collate nocase, list_id`
24374
+ ).all(accountId).map(storedListFromRow);
24375
+ }
24376
+ function resolveStoredXListSelector({
24377
+ account,
24378
+ listId,
24379
+ name
24380
+ }) {
24381
+ if (Boolean(listId) === Boolean(name)) {
24382
+ throw new Error("Choose exactly one of --list or --list-id");
24383
+ }
24384
+ const db = getReadDb();
24385
+ const accountId = accountIdForRead(db, account);
24386
+ const rows = listId ? db.prepare("select * from x_lists where account_id = ? and list_id = ?").all(accountId, listId) : db.prepare(
24387
+ "select * from x_lists where account_id = ? and name = ? collate nocase order by list_id"
24388
+ ).all(accountId, name);
24389
+ if (rows.length === 0) {
24390
+ throw new Error(
24391
+ `No cached List matches ${listId ? `id ${listId}` : `name ${name}`}; run birdclaw sync lists`
24392
+ );
24393
+ }
24394
+ if (rows.length > 1) {
24395
+ throw new Error(
24396
+ `List name ${name} is ambiguous; use --list-id (${rows.map((row) => String(row.list_id)).join(", ")})`
24397
+ );
24398
+ }
24399
+ return storedListFromRow(rows[0]);
24400
+ }
24401
+ function listStoredXListMembers({
24402
+ account,
24403
+ listId,
24404
+ name,
24405
+ includeEnded = false,
24406
+ limit = 100
24407
+ }) {
24408
+ const list = resolveStoredXListSelector({ account, listId, name });
24409
+ const db = getReadDb();
24410
+ const rows = db.prepare(
24411
+ `
24412
+ select
24413
+ member.account_id, member.list_id, member.external_user_id,
24414
+ member.current, member.first_seen_at, member.last_seen_at,
24415
+ member.ended_at, profile.id as profile_id, profile.handle,
24416
+ profile.display_name, profile.bio, profile.followers_count,
24417
+ profile.avatar_url
24418
+ from x_list_members member
24419
+ join profiles profile on profile.id = member.profile_id
24420
+ where member.account_id = ? and member.list_id = ?
24421
+ and (? = 1 or member.current = 1)
24422
+ order by profile.followers_count desc, profile.handle collate nocase
24423
+ limit ?
24424
+ `
24425
+ ).all(
24426
+ list.accountId,
24427
+ list.listId,
24428
+ includeEnded ? 1 : 0,
24429
+ positiveInteger("--limit", limit, 1e4)
24430
+ );
24431
+ const items = rows.map((row) => ({
24432
+ accountId: String(row.account_id),
24433
+ listId: String(row.list_id),
24434
+ externalUserId: String(row.external_user_id),
24435
+ current: Number(row.current) === 1,
24436
+ firstSeenAt: String(row.first_seen_at),
24437
+ lastSeenAt: String(row.last_seen_at),
24438
+ ...row.ended_at ? { endedAt: String(row.ended_at) } : {},
24439
+ profile: {
24440
+ id: String(row.profile_id),
24441
+ handle: String(row.handle),
24442
+ displayName: String(row.display_name),
24443
+ bio: String(row.bio),
24444
+ followersCount: Number(row.followers_count),
24445
+ ...row.avatar_url ? { avatarUrl: String(row.avatar_url) } : {}
24446
+ }
24447
+ }));
24448
+ return { list, items };
24449
+ }
24450
+
24451
+ // src/cli/register-lists.ts
24452
+ function registerListCommands({
24453
+ program: program2,
24454
+ print: print2,
24455
+ asJson,
24456
+ autoUpdateBeforeRead: autoUpdateBeforeRead2
24457
+ }) {
24458
+ const listsCommand = program2.command("lists").description("Inspect cached X Lists and membership");
24459
+ listsCommand.command("list").description("List cached owned X Lists").option("--account <accountId>", "Account id").action(async (options) => {
24460
+ await autoUpdateBeforeRead2();
24461
+ const items = listStoredXLists({ account: options.account });
24462
+ if (asJson()) {
24463
+ print2(items, true);
24464
+ return;
24465
+ }
24466
+ console.log(
24467
+ items.map(
24468
+ (item) => `${item.name} ${item.listId} ${item.membershipStatus} ${String(item.memberResultCount)}/${item.memberCount === void 0 ? "?" : String(item.memberCount)}`
24469
+ ).join("\n")
24470
+ );
24471
+ });
24472
+ 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) => {
24473
+ await autoUpdateBeforeRead2();
24474
+ const result = listStoredXListMembers({
24475
+ account: options.account,
24476
+ name,
24477
+ listId: options.listId,
24478
+ includeEnded: Boolean(options.includeEnded),
24479
+ limit: Number(options.limit)
24480
+ });
24481
+ if (asJson()) {
24482
+ print2(result, true);
24483
+ return;
24484
+ }
24485
+ console.log(
24486
+ result.items.map(
24487
+ (item) => `@${item.profile.handle} ${item.externalUserId} ${item.current ? "current" : "ended"}`
24488
+ ).join("\n")
24489
+ );
24490
+ });
24491
+ }
24492
+
23374
24493
  // src/lib/profile-replies.ts
23375
- import { Effect as Effect50 } from "effect";
24494
+ import { Effect as Effect51 } from "effect";
23376
24495
 
23377
24496
  // src/lib/moderation-target.ts
23378
- import { Effect as Effect49 } from "effect";
24497
+ import { Effect as Effect50 } from "effect";
23379
24498
 
23380
24499
  // src/lib/bird-actions.ts
23381
- import { Effect as Effect48 } from "effect";
24500
+ import { Effect as Effect49 } from "effect";
23382
24501
 
23383
24502
  // src/lib/bird-command.ts
23384
24503
  import { access } from "node:fs/promises";
23385
24504
  import { constants as constants2 } from "node:fs";
23386
- import { Data as Data3, Effect as Effect47 } from "effect";
24505
+ import { Data as Data3, Effect as Effect48 } from "effect";
23387
24506
  var BirdCommandUnavailableError = class extends Data3.TaggedError(
23388
24507
  "BirdCommandUnavailableError"
23389
24508
  ) {
@@ -23436,25 +24555,25 @@ function execFailureFromCause(command, cause) {
23436
24555
  }
23437
24556
  function assertBirdCommandAvailableEffect(command) {
23438
24557
  if (!isPathCommand(command)) {
23439
- return Effect47.void;
24558
+ return Effect48.void;
23440
24559
  }
23441
- return Effect47.tryPromise({
24560
+ return Effect48.tryPromise({
23442
24561
  try: () => Promise.resolve(access(command, constants2.X_OK)),
23443
24562
  catch: (cause) => new BirdCommandUnavailableError({
23444
24563
  message: formatBirdInstallHint(command),
23445
24564
  command,
23446
24565
  cause
23447
24566
  })
23448
- }).pipe(Effect47.asVoid);
24567
+ }).pipe(Effect48.asVoid);
23449
24568
  }
23450
24569
  function getBirdCommandEffect() {
23451
- return Effect47.try({
24570
+ return Effect48.try({
23452
24571
  try: () => getBirdCommand(),
23453
24572
  catch: (cause) => cause instanceof Error ? cause : new Error(String(cause))
23454
24573
  });
23455
24574
  }
23456
24575
  function runBirdCommandEffect(args, options) {
23457
- return Effect47.gen(function* () {
24576
+ return Effect48.gen(function* () {
23458
24577
  const birdCommand = yield* getBirdCommandEffect();
23459
24578
  yield* assertBirdCommandAvailableEffect(birdCommand);
23460
24579
  return yield* runSubprocessEffect({
@@ -23467,8 +24586,8 @@ function runBirdCommandEffect(args, options) {
23467
24586
  ...options?.signal ? { signal: options.signal } : {},
23468
24587
  ...options?.killSignal ? { killSignal: options.killSignal } : {}
23469
24588
  }).pipe(
23470
- Effect47.map(({ stdout, stderr }) => ({ stdout, stderr })),
23471
- Effect47.mapError((cause) => execFailureFromCause(birdCommand, cause))
24589
+ Effect48.map(({ stdout, stderr }) => ({ stdout, stderr })),
24590
+ Effect48.mapError((cause) => execFailureFromCause(birdCommand, cause))
23472
24591
  );
23473
24592
  });
23474
24593
  }
@@ -23506,9 +24625,9 @@ function toError18(error) {
23506
24625
  return error instanceof Error ? error : new Error(String(error));
23507
24626
  }
23508
24627
  function runBirdJsonCommandEffect2(args) {
23509
- return Effect48.gen(function* () {
24628
+ return Effect49.gen(function* () {
23510
24629
  const { stdout } = yield* runBirdCommandEffect(args);
23511
- return yield* Effect48.try({
24630
+ return yield* Effect49.try({
23512
24631
  try: () => JSON.parse(stripAnsi2(stdout)),
23513
24632
  catch: toError18
23514
24633
  });
@@ -23522,13 +24641,13 @@ function safeBirdProfileArg(query) {
23522
24641
  throw new Error("Invalid bird profile query");
23523
24642
  }
23524
24643
  function readBirdStatusViaBirdEffect(query) {
23525
- return Effect48.gen(function* () {
23526
- const safeQuery = yield* Effect48.try({
24644
+ return Effect49.gen(function* () {
24645
+ const safeQuery = yield* Effect49.try({
23527
24646
  try: () => safeBirdProfileArg(query),
23528
24647
  catch: toError18
23529
24648
  });
23530
24649
  return yield* runBirdJsonCommandEffect2(["status", safeQuery, "--json"]);
23531
- }).pipe(Effect48.catchAll(() => Effect48.succeed(null)));
24650
+ }).pipe(Effect49.catchAll(() => Effect49.succeed(null)));
23532
24651
  }
23533
24652
  function readBirdStatusViaBird(query) {
23534
24653
  return runEffectPromise(readBirdStatusViaBirdEffect(query));
@@ -23565,8 +24684,8 @@ function toBirdLookupUser(payload) {
23565
24684
  };
23566
24685
  }
23567
24686
  function lookupProfileViaBirdEffect2(query) {
23568
- return Effect48.gen(function* () {
23569
- const safeQuery = yield* Effect48.try({
24687
+ return Effect49.gen(function* () {
24688
+ const safeQuery = yield* Effect49.try({
23570
24689
  try: () => safeBirdProfileArg(query),
23571
24690
  catch: toError18
23572
24691
  });
@@ -23577,11 +24696,11 @@ function lookupProfileViaBirdEffect2(query) {
23577
24696
  "1",
23578
24697
  "--json"
23579
24698
  ]);
23580
- return yield* Effect48.try({
24699
+ return yield* Effect49.try({
23581
24700
  try: () => toBirdLookupUser(payload),
23582
24701
  catch: toError18
23583
24702
  });
23584
- }).pipe(Effect48.catchAll(() => Effect48.succeed(null)));
24703
+ }).pipe(Effect49.catchAll(() => Effect49.succeed(null)));
23585
24704
  }
23586
24705
  function lookupProfileViaBird(query) {
23587
24706
  return runEffectPromise(lookupProfileViaBirdEffect2(query));
@@ -23592,8 +24711,8 @@ function runVerifiedBirdMutationEffect({
23592
24711
  verifyField,
23593
24712
  expectedValue
23594
24713
  }) {
23595
- return Effect48.gen(function* () {
23596
- const safeQuery = yield* Effect48.try({
24714
+ return Effect49.gen(function* () {
24715
+ const safeQuery = yield* Effect49.try({
23597
24716
  try: () => safeBirdProfileArg(query),
23598
24717
  catch: toError18
23599
24718
  });
@@ -23607,12 +24726,12 @@ function runVerifiedBirdMutationEffect({
23607
24726
  action,
23608
24727
  safeQuery
23609
24728
  ]).pipe(
23610
- Effect48.map(({ stdout, stderr }) => ({
24729
+ Effect49.map(({ stdout, stderr }) => ({
23611
24730
  ok: true,
23612
24731
  output: normalizeOutput(stdout, stderr)
23613
24732
  })),
23614
- Effect48.catchAll(
23615
- (error) => Effect48.succeed({
24733
+ Effect49.catchAll(
24734
+ (error) => Effect49.succeed({
23616
24735
  ok: false,
23617
24736
  output: formatExecError2(error, `bird ${action} failed`)
23618
24737
  })
@@ -23691,7 +24810,7 @@ function toError19(error) {
23691
24810
  return error instanceof Error ? error : new Error(String(error));
23692
24811
  }
23693
24812
  function trySync14(try_) {
23694
- return Effect49.try({
24813
+ return Effect50.try({
23695
24814
  try: try_,
23696
24815
  catch: toError19
23697
24816
  });
@@ -23739,11 +24858,11 @@ function resolveLocalProfile(db, normalizedQuery) {
23739
24858
  };
23740
24859
  }
23741
24860
  function resolveProfileEffect(query, providedDb) {
23742
- return Effect49.gen(function* () {
24861
+ return Effect50.gen(function* () {
23743
24862
  const db = providedDb ?? (yield* trySync14(() => getNativeDb()));
23744
24863
  const normalizedQuery = normalizeProfileQuery(query);
23745
24864
  if (!normalizedQuery) {
23746
- return yield* Effect49.fail(new Error("Missing profile handle or id"));
24865
+ return yield* Effect50.fail(new Error("Missing profile handle or id"));
23747
24866
  }
23748
24867
  const local = yield* trySync14(
23749
24868
  () => resolveLocalProfile(db, normalizedQuery)
@@ -23752,7 +24871,7 @@ function resolveProfileEffect(query, providedDb) {
23752
24871
  if (local) {
23753
24872
  return local;
23754
24873
  }
23755
- return yield* Effect49.fail(
24874
+ return yield* Effect50.fail(
23756
24875
  new Error(`Profile not found locally: ${query}`)
23757
24876
  );
23758
24877
  }
@@ -23764,8 +24883,8 @@ function resolveProfileEffect(query, providedDb) {
23764
24883
  const birdResult = yield* tryPromise(
23765
24884
  () => lookupProfileViaBird(local?.profile.handle ?? normalizedQuery)
23766
24885
  ).pipe(
23767
- Effect49.map((value) => ({ ok: true, value })),
23768
- Effect49.catchAll((error) => Effect49.succeed({ ok: false, error }))
24886
+ Effect50.map((value) => ({ ok: true, value })),
24887
+ Effect50.catchAll((error) => Effect50.succeed({ ok: false, error }))
23769
24888
  );
23770
24889
  if (birdResult.ok) {
23771
24890
  user = birdResult.value ?? void 0;
@@ -23776,9 +24895,9 @@ function resolveProfileEffect(query, providedDb) {
23776
24895
  const xurlResult = yield* tryPromise(
23777
24896
  () => /^\d+$/.test(normalizedQuery) ? lookupUsersByIds([normalizedQuery]) : lookupUsersByHandles([local?.profile.handle ?? normalizedQuery])
23778
24897
  ).pipe(
23779
- Effect49.map((value) => ({ ok: true, value })),
23780
- Effect49.catchAll(
23781
- (error) => Effect49.succeed({ ok: false, error })
24898
+ Effect50.map((value) => ({ ok: true, value })),
24899
+ Effect50.catchAll(
24900
+ (error) => Effect50.succeed({ ok: false, error })
23782
24901
  )
23783
24902
  );
23784
24903
  if (xurlResult.ok) {
@@ -23791,13 +24910,13 @@ function resolveProfileEffect(query, providedDb) {
23791
24910
  if (local) {
23792
24911
  return local;
23793
24912
  }
23794
- return yield* Effect49.fail(lastError);
24913
+ return yield* Effect50.fail(lastError);
23795
24914
  }
23796
24915
  if (!user) {
23797
24916
  if (local) {
23798
24917
  return local;
23799
24918
  }
23800
- return yield* Effect49.fail(new Error(`Profile not found: ${query}`));
24919
+ return yield* Effect50.fail(new Error(`Profile not found: ${query}`));
23801
24920
  }
23802
24921
  if (local) {
23803
24922
  return yield* databaseWriteEffect(
@@ -23838,11 +24957,11 @@ function inspectProfileReplies(query, { limit = 12 } = {}) {
23838
24957
  return runEffectPromise(inspectProfileRepliesEffect(query, { limit }));
23839
24958
  }
23840
24959
  function inspectProfileRepliesEffect(query, { limit = 12 } = {}) {
23841
- return Effect50.gen(function* () {
24960
+ return Effect51.gen(function* () {
23842
24961
  const resolved = yield* tryPromise(() => resolveProfile(query));
23843
24962
  const externalUserId = resolved.externalUserId;
23844
24963
  if (!externalUserId) {
23845
- return yield* Effect50.fail(
24964
+ return yield* Effect51.fail(
23846
24965
  new Error(`Profile has no external Twitter user id: ${query}`)
23847
24966
  );
23848
24967
  }
@@ -23937,18 +25056,18 @@ function registerMentionCommands({
23937
25056
  }
23938
25057
 
23939
25058
  // src/lib/blocks.ts
23940
- import { Effect as Effect53 } from "effect";
25059
+ import { Effect as Effect54 } from "effect";
23941
25060
 
23942
25061
  // src/lib/moderation-state.ts
23943
- import { Effect as Effect52 } from "effect";
25062
+ import { Effect as Effect53 } from "effect";
23944
25063
 
23945
25064
  // src/lib/actions-transport.ts
23946
- import { Effect as Effect51 } from "effect";
25065
+ import { Effect as Effect52 } from "effect";
23947
25066
  function toError20(error) {
23948
25067
  return error instanceof Error ? error : new Error(String(error));
23949
25068
  }
23950
25069
  function trySync15(try_) {
23951
- return Effect51.try({
25070
+ return Effect52.try({
23952
25071
  try: try_,
23953
25072
  catch: toError20
23954
25073
  });
@@ -23960,7 +25079,7 @@ function liveWritesDisabled4() {
23960
25079
  return process.env.BIRDCLAW_DISABLE_LIVE_WRITES === "1";
23961
25080
  }
23962
25081
  function verifyExpectedAccountEffect(expectedAccount) {
23963
- return Effect51.gen(function* () {
25082
+ return Effect52.gen(function* () {
23964
25083
  if (!expectedAccount) return null;
23965
25084
  if (liveWritesDisabled4()) return null;
23966
25085
  const sourceUser = yield* tryPromise(() => lookupAuthenticatedUserFresh());
@@ -23970,7 +25089,7 @@ function verifyExpectedAccountEffect(expectedAccount) {
23970
25089
  const expectedHandle = profileHandleKey(expectedAccount.handle);
23971
25090
  if (expectedExternalUserId) {
23972
25091
  if (sourceUserId === expectedExternalUserId) return sourceUserId;
23973
- return yield* Effect51.fail(
25092
+ return yield* Effect52.fail(
23974
25093
  new Error(
23975
25094
  `xurl is authenticated as user ${sourceUserId || "unknown"}, not account ${expectedAccount.id}`
23976
25095
  )
@@ -23978,7 +25097,7 @@ function verifyExpectedAccountEffect(expectedAccount) {
23978
25097
  }
23979
25098
  if (expectedHandle && sourceUsername === expectedHandle)
23980
25099
  return sourceUserId;
23981
- return yield* Effect51.fail(
25100
+ return yield* Effect52.fail(
23982
25101
  new Error(
23983
25102
  sourceUsername ? `xurl is authenticated as @${sourceUsername}, not @${expectedHandle}` : "xurl authenticated user unavailable"
23984
25103
  )
@@ -23986,7 +25105,7 @@ function verifyExpectedAccountEffect(expectedAccount) {
23986
25105
  });
23987
25106
  }
23988
25107
  function runBirdActionEffect(action, query) {
23989
- return Effect51.gen(function* () {
25108
+ return Effect52.gen(function* () {
23990
25109
  const result = yield* tryPromise(
23991
25110
  () => action === "block" ? blockUserViaBird(query) : action === "unblock" ? unblockUserViaBird(query) : action === "mute" ? muteUserViaBird(query) : unmuteUserViaBird(query)
23992
25111
  );
@@ -24006,7 +25125,7 @@ function getVerifyExpectation(action) {
24006
25125
  };
24007
25126
  }
24008
25127
  function runXurlActionEffect(action, query, targetUserId, verifiedSourceUserId) {
24009
- return Effect51.gen(function* () {
25128
+ return Effect52.gen(function* () {
24010
25129
  if (!targetUserId) {
24011
25130
  return {
24012
25131
  ok: false,
@@ -24071,13 +25190,13 @@ function runModerationActionEffect({
24071
25190
  transport,
24072
25191
  expectedAccount
24073
25192
  }) {
24074
- return Effect51.gen(function* () {
25193
+ return Effect52.gen(function* () {
24075
25194
  const requestedTransport = yield* trySync15(
24076
25195
  () => resolveActionsTransport(transport)
24077
25196
  );
24078
25197
  const verifyXurlAccount = () => verifyExpectedAccountEffect(expectedAccount).pipe(
24079
- Effect51.catchAll(
24080
- (error) => Effect51.succeed({
25198
+ Effect52.catchAll(
25199
+ (error) => Effect52.succeed({
24081
25200
  ok: false,
24082
25201
  output: error instanceof Error ? error.message : String(error),
24083
25202
  transport: "xurl"
@@ -24166,7 +25285,7 @@ function toError21(error) {
24166
25285
  return error instanceof Error ? error : new Error(String(error));
24167
25286
  }
24168
25287
  function trySync16(try_) {
24169
- return Effect52.try({
25288
+ return Effect53.try({
24170
25289
  try: try_,
24171
25290
  catch: toError21
24172
25291
  });
@@ -24285,20 +25404,20 @@ function resolveModerationTargetEffect({
24285
25404
  query,
24286
25405
  selfActionError
24287
25406
  }) {
24288
- return Effect52.gen(function* () {
25407
+ return Effect53.gen(function* () {
24289
25408
  const db = yield* trySync16(() => getNativeDb());
24290
25409
  const resolvedAccountId = accountId || getDefaultAccountId(db);
24291
25410
  const accountHandle = yield* trySync16(
24292
25411
  () => getAccountHandle(db, resolvedAccountId)
24293
25412
  );
24294
25413
  if (!accountHandle) {
24295
- return yield* Effect52.fail(
25414
+ return yield* Effect53.fail(
24296
25415
  new Error(`Unknown account: ${resolvedAccountId}`)
24297
25416
  );
24298
25417
  }
24299
25418
  const normalizedQuery = normalizeProfileQuery(query);
24300
25419
  if (normalizedQuery.toLowerCase() === accountHandle.toLowerCase()) {
24301
- return yield* Effect52.fail(new Error(selfActionError));
25420
+ return yield* Effect53.fail(new Error(selfActionError));
24302
25421
  }
24303
25422
  const resolved = yield* resolveProfileEffect(query, db);
24304
25423
  const account = yield* trySync16(
@@ -24309,7 +25428,7 @@ function resolveModerationTargetEffect({
24309
25428
  );
24310
25429
  const accountExternalUserId = account?.external_user_id ?? getExternalUserId(accountProfile?.id ?? "");
24311
25430
  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));
25431
+ return yield* Effect53.fail(new Error(selfActionError));
24313
25432
  }
24314
25433
  return {
24315
25434
  db,
@@ -24326,7 +25445,7 @@ function resolveModerationTargetEffect({
24326
25445
  }
24327
25446
  function addModerationEffect(kind, accountId, query, options = {}) {
24328
25447
  const descriptor = moderationDescriptors[kind];
24329
- return Effect52.gen(function* () {
25448
+ return Effect53.gen(function* () {
24330
25449
  const { db, resolved, resolvedAccountId, accountIdentity, actionQuery } = yield* resolveModerationTargetEffect({
24331
25450
  accountId,
24332
25451
  query,
@@ -24376,7 +25495,7 @@ function addModeration(kind, accountId, query, options = {}) {
24376
25495
  }
24377
25496
  function recordModerationEffect(kind, accountId, query) {
24378
25497
  const descriptor = moderationDescriptors[kind];
24379
- return Effect52.gen(function* () {
25498
+ return Effect53.gen(function* () {
24380
25499
  const { db, resolved, resolvedAccountId } = yield* resolveModerationTargetEffect({
24381
25500
  accountId,
24382
25501
  query,
@@ -24407,7 +25526,7 @@ function recordModeration(kind, accountId, query) {
24407
25526
  }
24408
25527
  function removeModerationEffect(kind, accountId, query, options = {}) {
24409
25528
  const descriptor = moderationDescriptors[kind];
24410
- return Effect52.gen(function* () {
25529
+ return Effect53.gen(function* () {
24411
25530
  const { db, resolved, resolvedAccountId, accountIdentity, actionQuery } = yield* resolveModerationTargetEffect({
24412
25531
  accountId,
24413
25532
  query,
@@ -24483,7 +25602,7 @@ function toError22(error) {
24483
25602
  return error instanceof Error ? error : new Error(String(error));
24484
25603
  }
24485
25604
  function trySync17(try_) {
24486
- return Effect53.try({
25605
+ return Effect54.try({
24487
25606
  try: try_,
24488
25607
  catch: toError22
24489
25608
  });
@@ -24496,12 +25615,12 @@ function listBlocks({
24496
25615
  return listModerationState("block", { account, search, limit });
24497
25616
  }
24498
25617
  function syncBlocksEffect(accountId) {
24499
- return Effect53.gen(function* () {
25618
+ return Effect54.gen(function* () {
24500
25619
  const db = yield* trySync17(() => getNativeDb());
24501
25620
  const resolvedAccountId = accountId || getDefaultAccountId(db);
24502
25621
  const accountHandle = getAccountHandle(db, resolvedAccountId);
24503
25622
  if (!accountHandle) {
24504
- return yield* Effect53.fail(
25623
+ return yield* Effect54.fail(
24505
25624
  new Error(`Unknown account: ${resolvedAccountId}`)
24506
25625
  );
24507
25626
  }
@@ -24522,9 +25641,9 @@ function syncBlocksEffect(accountId) {
24522
25641
  }
24523
25642
  };
24524
25643
  }
24525
- return yield* Effect53.gen(function* () {
25644
+ return yield* Effect54.gen(function* () {
24526
25645
  const me = yield* tryPromise(() => lookupAuthenticatedUserFresh()).pipe(
24527
- Effect53.mapError(toError22)
25646
+ Effect54.mapError(toError22)
24528
25647
  );
24529
25648
  const sourceUserId = typeof me?.id === "string" && me.id.length > 0 ? me.id : null;
24530
25649
  const sourceUsername = typeof me?.username === "string" ? me.username.replace(/^@/, "") : "";
@@ -24571,7 +25690,7 @@ function syncBlocksEffect(accountId) {
24571
25690
  do {
24572
25691
  const page = yield* tryPromise(
24573
25692
  () => listBlockedUsers(sourceUserId, nextToken ?? void 0)
24574
- ).pipe(Effect53.mapError(toError22));
25693
+ ).pipe(Effect54.mapError(toError22));
24575
25694
  const pageProfileIds = yield* databaseWriteEffect((writeDb) => {
24576
25695
  return page.items.map((user) => {
24577
25696
  const resolved = upsertProfileFromXUser(writeDb, user);
@@ -24613,8 +25732,8 @@ function syncBlocksEffect(accountId) {
24613
25732
  }
24614
25733
  };
24615
25734
  }).pipe(
24616
- Effect53.catchAll(
24617
- (error) => Effect53.succeed({
25735
+ Effect54.catchAll(
25736
+ (error) => Effect54.succeed({
24618
25737
  ok: false,
24619
25738
  accountId: resolvedAccountId,
24620
25739
  synced: remoteProfileIds.length > 0,
@@ -24765,7 +25884,7 @@ function registerModerationCommands({
24765
25884
 
24766
25885
  // src/lib/blocklist.ts
24767
25886
  import { readFile } from "node:fs/promises";
24768
- import { Effect as Effect54 } from "effect";
25887
+ import { Effect as Effect55 } from "effect";
24769
25888
  function stripInlineCode(value) {
24770
25889
  const match = value.match(/`([^`]+)`/);
24771
25890
  return match?.[1] ?? value;
@@ -24802,15 +25921,15 @@ function parseBlocklistText(text) {
24802
25921
  return items;
24803
25922
  }
24804
25923
  function importBlocklistEffect(accountId, filePath) {
24805
- return Effect54.gen(function* () {
25924
+ return Effect55.gen(function* () {
24806
25925
  const text = yield* tryPromise(() => readFile(filePath, "utf8"));
24807
25926
  const queries = parseBlocklistText(text);
24808
25927
  const items = [];
24809
25928
  for (const query of queries) {
24810
25929
  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 })
25930
+ Effect55.map((result2) => ({ ok: true, result: result2 })),
25931
+ Effect55.catchAll(
25932
+ (error) => Effect55.succeed({ ok: false, error })
24814
25933
  )
24815
25934
  );
24816
25935
  if (!blocked.ok) {
@@ -24866,7 +25985,7 @@ function registerModerationCommands2(context) {
24866
25985
  }
24867
25986
 
24868
25987
  // src/lib/link-index.ts
24869
- import { Effect as Effect55 } from "effect";
25988
+ import { Effect as Effect56 } from "effect";
24870
25989
  var DEFAULT_EXPAND_CONCURRENCY = 12;
24871
25990
  function parseJsonField5(value, fallback) {
24872
25991
  if (typeof value !== "string" || value.length === 0) {
@@ -25028,7 +26147,7 @@ function expandWithConcurrencyEffect(db, urls, options) {
25028
26147
  misses: 0,
25029
26148
  errors: 0
25030
26149
  };
25031
- return Effect55.forEach(
26150
+ return Effect56.forEach(
25032
26151
  urls,
25033
26152
  (url) => tryPromise(
25034
26153
  () => expandUrls([url], {
@@ -25037,7 +26156,7 @@ function expandWithConcurrencyEffect(db, urls, options) {
25037
26156
  timeoutMs: options.timeoutMs
25038
26157
  })
25039
26158
  ).pipe(
25040
- Effect55.map((results) => {
26159
+ Effect56.map((results) => {
25041
26160
  const result = results[0];
25042
26161
  if (result.source === "network") {
25043
26162
  counts.networkExpansions++;
@@ -25054,10 +26173,10 @@ function expandWithConcurrencyEffect(db, urls, options) {
25054
26173
  })
25055
26174
  ),
25056
26175
  { concurrency }
25057
- ).pipe(Effect55.as(counts));
26176
+ ).pipe(Effect56.as(counts));
25058
26177
  }
25059
26178
  function backfillLinkIndexEffect(options = {}) {
25060
- return Effect55.gen(function* () {
26179
+ return Effect56.gen(function* () {
25061
26180
  const db = getNativeDb({ seedDemoData: false });
25062
26181
  const { occurrences, entityExpansions } = rebuildOccurrences(
25063
26182
  db,
@@ -25352,7 +26471,7 @@ import {
25352
26471
  } from "node:fs";
25353
26472
  import { appendFile, copyFile, rename, rm, writeFile } from "node:fs/promises";
25354
26473
  import path11 from "node:path";
25355
- import { Effect as Effect56 } from "effect";
26474
+ import { Effect as Effect57 } from "effect";
25356
26475
  var DEFAULT_MAX_BYTES = 100 * 1024 * 1024;
25357
26476
  var PBS_PREFIXES = [
25358
26477
  "/media/",
@@ -25371,13 +26490,13 @@ function toMediaError(error) {
25371
26490
  return error instanceof Error ? error : new Error(String(error));
25372
26491
  }
25373
26492
  function tryMediaSync(try_) {
25374
- return Effect56.try({
26493
+ return Effect57.try({
25375
26494
  try: try_,
25376
26495
  catch: toMediaError
25377
26496
  });
25378
26497
  }
25379
26498
  function tryMediaPromise(try_) {
25380
- return Effect56.tryPromise({
26499
+ return Effect57.tryPromise({
25381
26500
  try: try_,
25382
26501
  catch: toMediaError
25383
26502
  });
@@ -25642,7 +26761,7 @@ function archivePathFor(item, mediaOriginalsDir) {
25642
26761
  return archiveTweetDirs(mediaOriginalsDir, item.tweetId).map((tweetDir) => path11.join(tweetDir, fileName)).find((archivePath) => existsSync5(archivePath)) ?? null;
25643
26762
  }
25644
26763
  function reuseFromArchiveEffect(item, mediaOriginalsDir, maxBytes) {
25645
- return Effect56.gen(function* () {
26764
+ return Effect57.gen(function* () {
25646
26765
  const archivePath = archivePathFor(item, mediaOriginalsDir);
25647
26766
  if (!archivePath || !existsSync5(archivePath)) return null;
25648
26767
  const bytes = fileSize(archivePath);
@@ -25669,15 +26788,15 @@ function contentRangeTotal(response) {
25669
26788
  return total ? Number(total) : null;
25670
26789
  }
25671
26790
  function writeResponseBodyEffect(response, tmpPath, append, maxBytes, initialBytes) {
25672
- return Effect56.gen(function* () {
26791
+ return Effect57.gen(function* () {
25673
26792
  if (!response.body)
25674
- return yield* Effect56.fail(new Error("missing response body"));
26793
+ return yield* Effect57.fail(new Error("missing response body"));
25675
26794
  let bytes = 0;
25676
26795
  if (!append) {
25677
26796
  yield* tryMediaPromise(() => writeFile(tmpPath, Buffer.alloc(0)));
25678
26797
  }
25679
26798
  const reader = response.body.getReader();
25680
- const result = yield* Effect56.gen(function* () {
26799
+ const result = yield* Effect57.gen(function* () {
25681
26800
  for (; ; ) {
25682
26801
  const { done, value } = yield* tryMediaPromise(() => reader.read());
25683
26802
  if (done) {
@@ -25687,31 +26806,31 @@ function writeResponseBodyEffect(response, tmpPath, append, maxBytes, initialByt
25687
26806
  bytes += chunk.length;
25688
26807
  if (initialBytes + bytes > maxBytes) {
25689
26808
  yield* tryMediaPromise(() => rm(tmpPath, { force: true })).pipe(
25690
- Effect56.catchAll(() => Effect56.void)
26809
+ Effect57.catchAll(() => Effect57.void)
25691
26810
  );
25692
- return yield* Effect56.fail(new Error("max-bytes"));
26811
+ return yield* Effect57.fail(new Error("max-bytes"));
25693
26812
  }
25694
26813
  yield* tryMediaPromise(() => appendFile(tmpPath, chunk));
25695
26814
  }
25696
26815
  return bytes;
25697
26816
  }).pipe(
25698
- Effect56.map((writtenBytes) => ({ ok: true, writtenBytes })),
25699
- Effect56.catchAll(
25700
- (error) => Effect56.gen(function* () {
26817
+ Effect57.map((writtenBytes) => ({ ok: true, writtenBytes })),
26818
+ Effect57.catchAll(
26819
+ (error) => Effect57.gen(function* () {
25701
26820
  if (error.message === "max-bytes") {
25702
26821
  yield* tryMediaPromise(() => rm(tmpPath, { force: true })).pipe(
25703
- Effect56.catchAll(() => Effect56.void)
26822
+ Effect57.catchAll(() => Effect57.void)
25704
26823
  );
25705
26824
  }
25706
26825
  yield* tryMediaPromise(() => reader.cancel(error)).pipe(
25707
- Effect56.catchAll(() => Effect56.void)
26826
+ Effect57.catchAll(() => Effect57.void)
25708
26827
  );
25709
26828
  return { error, ok: false };
25710
26829
  })
25711
26830
  ),
25712
- Effect56.ensuring(Effect56.sync(() => reader.releaseLock()))
26831
+ Effect57.ensuring(Effect57.sync(() => reader.releaseLock()))
25713
26832
  );
25714
- if (!result.ok) return yield* Effect56.fail(result.error);
26833
+ if (!result.ok) return yield* Effect57.fail(result.error);
25715
26834
  return result.writtenBytes;
25716
26835
  });
25717
26836
  }
@@ -25723,13 +26842,13 @@ function fetchOneEffect({
25723
26842
  userAgent,
25724
26843
  maxBytes
25725
26844
  }) {
25726
- return Effect56.gen(function* () {
26845
+ return Effect57.gen(function* () {
25727
26846
  let rateLimited = false;
25728
26847
  for (let attempt = 0; attempt <= retryMax; attempt += 1) {
25729
26848
  const partialBytes = item.kind === "image" ? 0 : fileSize(item.tmpPath);
25730
26849
  if (partialBytes > maxBytes) {
25731
26850
  yield* tryMediaPromise(() => rm(item.tmpPath, { force: true })).pipe(
25732
- Effect56.catchAll(() => Effect56.void)
26851
+ Effect57.catchAll(() => Effect57.void)
25733
26852
  );
25734
26853
  return fail(item, "max-bytes");
25735
26854
  }
@@ -25741,9 +26860,9 @@ function fetchOneEffect({
25741
26860
  }
25742
26861
  })
25743
26862
  ).pipe(
25744
- Effect56.map((response2) => ({ ok: true, response: response2 })),
25745
- Effect56.catchAll(
25746
- (error) => Effect56.succeed({ error, ok: false })
26863
+ Effect57.map((response2) => ({ ok: true, response: response2 })),
26864
+ Effect57.catchAll(
26865
+ (error) => Effect57.succeed({ error, ok: false })
25747
26866
  )
25748
26867
  );
25749
26868
  if (!responseResult.ok) {
@@ -25754,7 +26873,7 @@ function fetchOneEffect({
25754
26873
  rateLimited = true;
25755
26874
  if (attempt < retryMax) {
25756
26875
  yield* tryMediaPromise(() => sleep(1e3 * 2 ** attempt)).pipe(
25757
- Effect56.catchAll(() => Effect56.void)
26876
+ Effect57.catchAll(() => Effect57.void)
25758
26877
  );
25759
26878
  continue;
25760
26879
  }
@@ -25766,7 +26885,7 @@ function fetchOneEffect({
25766
26885
  const expectedTotal = contentRangeTotal(response) ?? (contentLength(response) ?? 0) + (response.status === 206 ? partialBytes : 0);
25767
26886
  if (expectedTotal > maxBytes) {
25768
26887
  yield* tryMediaPromise(() => rm(item.tmpPath, { force: true })).pipe(
25769
- Effect56.catchAll(() => Effect56.void)
26888
+ Effect57.catchAll(() => Effect57.void)
25770
26889
  );
25771
26890
  return fail(item, "max-bytes");
25772
26891
  }
@@ -25778,9 +26897,9 @@ function fetchOneEffect({
25778
26897
  maxBytes,
25779
26898
  append ? partialBytes : 0
25780
26899
  ).pipe(
25781
- Effect56.map((bytes) => ({ bytes, ok: true })),
25782
- Effect56.catchAll(
25783
- (error) => Effect56.succeed({ error, ok: false })
26900
+ Effect57.map((bytes) => ({ bytes, ok: true })),
26901
+ Effect57.catchAll(
26902
+ (error) => Effect57.succeed({ error, ok: false })
25784
26903
  )
25785
26904
  );
25786
26905
  if (!bytesResult.ok) {
@@ -25854,7 +26973,7 @@ function runGroupEffect(items, parallel, pacingMs, now2, sleep, worker) {
25854
26973
  );
25855
26974
  })
25856
26975
  );
25857
- return Effect56.forEach(pacedItems, runPaced, {
26976
+ return Effect57.forEach(pacedItems, runPaced, {
25858
26977
  concurrency: Math.min(parallel, items.length),
25859
26978
  discard: true
25860
26979
  });
@@ -25863,7 +26982,7 @@ function fetchTweetMedia(options = {}) {
25863
26982
  return runEffectPromise(fetchTweetMediaEffect(options));
25864
26983
  }
25865
26984
  function fetchTweetMediaEffect(options = {}) {
25866
- return Effect56.gen(function* () {
26985
+ return Effect57.gen(function* () {
25867
26986
  const now2 = options.now ?? Date.now;
25868
26987
  const startedAt = now2();
25869
26988
  const sleep = options.sleep ?? defaultSleep;
@@ -25930,7 +27049,7 @@ function fetchTweetMediaEffect(options = {}) {
25930
27049
  retryMax,
25931
27050
  userAgent,
25932
27051
  maxBytes
25933
- }).pipe(Effect56.map((fetched) => applyFetched(result, fetched)))
27052
+ }).pipe(Effect57.map((fetched) => applyFetched(result, fetched)))
25934
27053
  );
25935
27054
  yield* runGroupEffect(
25936
27055
  httpCandidates.filter((item) => item.kind === "image"),
@@ -25978,9 +27097,9 @@ function formatMediaFetchResult(result) {
25978
27097
  }
25979
27098
 
25980
27099
  // src/lib/whois.ts
25981
- import { Effect as Effect57 } from "effect";
27100
+ import { Effect as Effect58 } from "effect";
25982
27101
  function trySync18(try_) {
25983
- return Effect57.try({
27102
+ return Effect58.try({
25984
27103
  try: try_,
25985
27104
  catch: (cause) => cause
25986
27105
  });
@@ -26511,7 +27630,7 @@ function loadWhoisConversations(query, options, includeDms, context, limit) {
26511
27630
  return [...merged.values()];
26512
27631
  }
26513
27632
  function runWhoisEffect(query, options = {}) {
26514
- return Effect57.gen(function* () {
27633
+ return Effect58.gen(function* () {
26515
27634
  const includeDms = options.dms ?? true;
26516
27635
  const includeTweets = options.tweets ?? false;
26517
27636
  const limit = options.limit ?? 10;
@@ -26758,7 +27877,7 @@ function registerSearchCommands({
26758
27877
  parsePositiveIntegerOption: parsePositiveIntegerOption2
26759
27878
  }) {
26760
27879
  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(
27880
+ 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
27881
  "--min-likes <n>",
26763
27882
  "Override the low-quality like threshold (default 50)"
26764
27883
  ).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 +27887,18 @@ function registerSearchCommands({
26768
27887
  );
26769
27888
  if (options.minLikes !== void 0 && minLikes === void 0) return;
26770
27889
  await autoUpdateBeforeRead2();
27890
+ const selectedList = options.list || options.listId ? resolveStoredXListSelector({
27891
+ account: options.account,
27892
+ name: options.list,
27893
+ listId: options.listId
27894
+ }) : void 0;
26771
27895
  const replyFilter = options.replied ? "replied" : options.unreplied ? "unreplied" : "all";
26772
27896
  print2(
26773
27897
  listTimelineItems({
26774
27898
  resource: options.resource === "mentions" ? "mentions" : options.resource === "authored" ? "authored" : "home",
27899
+ account: options.account,
27900
+ listAccountId: selectedList?.accountId,
27901
+ listId: selectedList?.listId,
26775
27902
  search: query,
26776
27903
  replyFilter,
26777
27904
  since: options.since,
@@ -26954,7 +28081,7 @@ import { Readable as Readable2 } from "node:stream";
26954
28081
  import { pathToFileURL } from "node:url";
26955
28082
 
26956
28083
  // src/lib/http-effect.ts
26957
- import { Effect as Effect58 } from "effect";
28084
+ import { Effect as Effect59 } from "effect";
26958
28085
  var LOCAL_WEB_PEER_HEADER = "x-birdclaw-local-peer";
26959
28086
 
26960
28087
  // src/lib/production-server.ts
@@ -27228,7 +28355,7 @@ function registerStorageCommands({
27228
28355
  }
27229
28356
 
27230
28357
  // src/lib/authored-live.ts
27231
- import { Effect as Effect59 } from "effect";
28358
+ import { Effect as Effect60 } from "effect";
27232
28359
  var AuthoredSyncError = class extends Error {
27233
28360
  constructor(message, exitCode) {
27234
28361
  super(message);
@@ -27470,10 +28597,10 @@ function resolveAuthoredIdentityEffect({
27470
28597
  account,
27471
28598
  db
27472
28599
  }) {
27473
- return Effect59.gen(function* () {
28600
+ return Effect60.gen(function* () {
27474
28601
  const status = yield* liveTransportGateway.xurl.getTransportStatus();
27475
28602
  if (status.availableTransport !== "xurl") {
27476
- return yield* Effect59.fail(new AuthoredSyncError(status.statusText, 4));
28603
+ return yield* Effect60.fail(new AuthoredSyncError(status.statusText, 4));
27477
28604
  }
27478
28605
  const resolvedAccount = yield* trySync(
27479
28606
  () => resolveLiveSyncAccount(db, account)
@@ -27488,7 +28615,7 @@ function resolveAuthoredIdentityEffect({
27488
28615
  const authenticated = yield* liveTransportGateway.xurl.lookupAuthenticatedUser();
27489
28616
  const authenticatedUser = userFromAuthenticatedPayload(authenticated);
27490
28617
  if (!authenticatedUser?.id) {
27491
- return yield* Effect59.fail(
28618
+ return yield* Effect60.fail(
27492
28619
  new AuthoredSyncError(
27493
28620
  "Could not resolve authenticated Twitter user id",
27494
28621
  4
@@ -27496,7 +28623,7 @@ function resolveAuthoredIdentityEffect({
27496
28623
  );
27497
28624
  }
27498
28625
  if (normalizeUsername(authenticatedUser.username) !== normalizeUsername(resolvedAccount.username)) {
27499
- return yield* Effect59.fail(
28626
+ return yield* Effect60.fail(
27500
28627
  new AuthoredSyncError(
27501
28628
  `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
28629
  4
@@ -27795,9 +28922,9 @@ function syncAuthoredTweetsEffect({
27795
28922
  sinceId,
27796
28923
  untilId
27797
28924
  }) {
27798
- return Effect59.gen(function* () {
28925
+ return Effect60.gen(function* () {
27799
28926
  if (mode !== "xurl") {
27800
- return yield* Effect59.fail(
28927
+ return yield* Effect60.fail(
27801
28928
  new Error("authored sync only supports --mode xurl")
27802
28929
  );
27803
28930
  }
@@ -27860,7 +28987,7 @@ function syncAuthoredTweetsEffect({
27860
28987
  sourceUser
27861
28988
  })
27862
28989
  ).pipe(
27863
- Effect59.flatMap(
28990
+ Effect60.flatMap(
27864
28991
  () => trySync(() => {
27865
28992
  newestSeenId = maxTweetId(
27866
28993
  newestSeenId,
@@ -27963,6 +29090,32 @@ function registerSyncCommands({
27963
29090
  autoSyncAfterWrite: autoSyncAfterWrite2
27964
29091
  }) {
27965
29092
  const syncCommand = program2.command("sync").description("Refresh live Twitter collections into the local store");
29093
+ 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) => {
29094
+ try {
29095
+ const result = await syncXLists({
29096
+ account: options.account,
29097
+ mode: options.mode,
29098
+ maxLists: Number(options.maxLists),
29099
+ memberLimit: Number(options.memberLimit),
29100
+ maxMemberPages: Number(options.maxMemberPages),
29101
+ delayMs: Number(options.delayMs)
29102
+ });
29103
+ await autoSyncAfterWrite2();
29104
+ print2(result, true);
29105
+ if (!result.ok) process.exitCode = 5;
29106
+ } catch (error) {
29107
+ print2(
29108
+ {
29109
+ ok: false,
29110
+ kind: "lists",
29111
+ mode: options.mode,
29112
+ error: errorMessage(error)
29113
+ },
29114
+ true
29115
+ );
29116
+ process.exitCode = 1;
29117
+ }
29118
+ });
27966
29119
  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
29120
  const result = await syncHomeTimeline({
27968
29121
  account: options.account,
@@ -28137,6 +29290,7 @@ registerMentionCommands(commandContext);
28137
29290
  registerDirectMessageCommands(commandContext);
28138
29291
  registerSyncCommands(commandContext);
28139
29292
  registerJobCommands(commandContext);
29293
+ registerListCommands(commandContext);
28140
29294
  registerModerationCommands2(commandContext);
28141
29295
  registerComposeCommands(commandContext);
28142
29296
  registerInboxCommand(commandContext);