birdclaw 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +33 -1
  2. package/README.md +108 -7
  3. package/package.json +24 -23
  4. package/playwright.config.ts +1 -0
  5. package/public/favicon.ico +0 -0
  6. package/public/logo192.png +0 -0
  7. package/public/logo512.png +0 -0
  8. package/public/manifest.json +2 -2
  9. package/src/cli.ts +450 -18
  10. package/src/components/AppNav.tsx +66 -29
  11. package/src/components/AvatarChip.tsx +10 -5
  12. package/src/components/ConversationThread.tsx +125 -0
  13. package/src/components/DmWorkspace.tsx +96 -98
  14. package/src/components/EmbeddedTweetCard.tsx +20 -14
  15. package/src/components/InboxCard.tsx +92 -90
  16. package/src/components/LinkPreviewCard.tsx +270 -0
  17. package/src/components/ProfilePreview.tsx +8 -3
  18. package/src/components/SavedTimelineView.tsx +48 -38
  19. package/src/components/TimelineCard.tsx +228 -84
  20. package/src/components/TweetRichText.tsx +6 -2
  21. package/src/lib/archive-import.ts +1565 -65
  22. package/src/lib/authored-live.ts +1074 -0
  23. package/src/lib/backup.ts +316 -14
  24. package/src/lib/bird-actions.ts +1 -10
  25. package/src/lib/bird-command.ts +57 -0
  26. package/src/lib/bird.ts +89 -5
  27. package/src/lib/config.ts +1 -1
  28. package/src/lib/db.ts +191 -4
  29. package/src/lib/follow-graph.ts +1053 -0
  30. package/src/lib/link-index.ts +11 -98
  31. package/src/lib/link-insights.ts +834 -0
  32. package/src/lib/link-preview-metadata.ts +334 -0
  33. package/src/lib/media-fetch.ts +787 -0
  34. package/src/lib/media-includes.ts +165 -0
  35. package/src/lib/mention-threads-live.ts +535 -43
  36. package/src/lib/mentions-live.ts +623 -19
  37. package/src/lib/moderation-target.ts +6 -0
  38. package/src/lib/profile-hydration.ts +1 -1
  39. package/src/lib/profile-resolver.ts +115 -1
  40. package/src/lib/queries.ts +233 -7
  41. package/src/lib/seed.ts +127 -8
  42. package/src/lib/timeline-collections-live.ts +145 -22
  43. package/src/lib/timeline-live.ts +10 -15
  44. package/src/lib/tweet-account-edges.ts +9 -2
  45. package/src/lib/tweet-render.ts +97 -0
  46. package/src/lib/types.ts +185 -2
  47. package/src/lib/ui.ts +375 -147
  48. package/src/lib/url-expansion-store.ts +110 -0
  49. package/src/lib/url-expansion.ts +20 -3
  50. package/src/lib/x-profile.ts +7 -2
  51. package/src/lib/xurl.ts +296 -12
  52. package/src/routeTree.gen.ts +105 -0
  53. package/src/routes/__root.tsx +7 -3
  54. package/src/routes/api/conversation.tsx +34 -0
  55. package/src/routes/api/link-insights.tsx +79 -0
  56. package/src/routes/api/link-preview.tsx +43 -0
  57. package/src/routes/api/profile-hydrate.tsx +51 -0
  58. package/src/routes/blocks.tsx +111 -86
  59. package/src/routes/dms.tsx +90 -78
  60. package/src/routes/inbox.tsx +98 -86
  61. package/src/routes/index.tsx +63 -50
  62. package/src/routes/links.tsx +883 -0
  63. package/src/routes/mentions.tsx +63 -50
  64. package/src/styles.css +106 -43
package/src/lib/db.ts CHANGED
@@ -20,6 +20,7 @@ export interface ProfilesTable {
20
20
  bio: string;
21
21
  followers_count: number;
22
22
  following_count: number;
23
+ public_metrics_json: string;
23
24
  avatar_hue: number;
24
25
  avatar_url: string | null;
25
26
  location: string | null;
@@ -116,7 +117,7 @@ export interface TweetCollectionsTable {
116
117
  export interface TweetAccountEdgesTable {
117
118
  account_id: string;
118
119
  tweet_id: string;
119
- kind: "home" | "mention";
120
+ kind: "home" | "mention" | "authored" | "thread_context";
120
121
  first_seen_at: string;
121
122
  last_seen_at: string;
122
123
  seen_count: number;
@@ -194,6 +195,8 @@ export interface UrlExpansionsTable {
194
195
  expanded_handle: string | null;
195
196
  title: string | null;
196
197
  description: string | null;
198
+ image_url: string | null;
199
+ site_name: string | null;
197
200
  error: string | null;
198
201
  source: string;
199
202
  updated_at: string;
@@ -210,6 +213,50 @@ export interface LinkOccurrencesTable {
210
213
  created_at: string;
211
214
  }
212
215
 
216
+ export interface FollowEdgesTable {
217
+ account_id: string;
218
+ direction: "followers" | "following";
219
+ profile_id: string;
220
+ external_user_id: string;
221
+ source: string;
222
+ current: number;
223
+ first_seen_at: string;
224
+ last_seen_at: string;
225
+ ended_at: string | null;
226
+ updated_at: string;
227
+ }
228
+
229
+ export interface FollowSnapshotsTable {
230
+ id: string;
231
+ account_id: string;
232
+ direction: "followers" | "following";
233
+ source: string;
234
+ status: "complete" | "incomplete";
235
+ page_count: number;
236
+ result_count: number;
237
+ started_at: string;
238
+ completed_at: string;
239
+ raw_meta_json: string;
240
+ }
241
+
242
+ export interface FollowSnapshotMembersTable {
243
+ snapshot_id: string;
244
+ profile_id: string;
245
+ external_user_id: string;
246
+ position: number;
247
+ }
248
+
249
+ export interface FollowEventsTable {
250
+ id: string;
251
+ account_id: string;
252
+ direction: "followers" | "following";
253
+ profile_id: string;
254
+ external_user_id: string;
255
+ kind: "started" | "ended";
256
+ event_at: string;
257
+ snapshot_id: string;
258
+ }
259
+
213
260
  export interface BirdclawDatabase {
214
261
  accounts: AccountsTable;
215
262
  profiles: ProfilesTable;
@@ -229,10 +276,15 @@ export interface BirdclawDatabase {
229
276
  sync_cache: SyncCacheTable;
230
277
  url_expansions: UrlExpansionsTable;
231
278
  link_occurrences: LinkOccurrencesTable;
279
+ follow_edges: FollowEdgesTable;
280
+ follow_snapshots: FollowSnapshotsTable;
281
+ follow_snapshot_members: FollowSnapshotMembersTable;
282
+ follow_events: FollowEventsTable;
232
283
  }
233
284
 
234
285
  let nativeDb: Database | undefined;
235
286
  let kyselyDb: Kysely<BirdclawDatabase> | undefined;
287
+ let demoSeedAttempted = false;
236
288
 
237
289
  export interface InitDatabaseOptions {
238
290
  seedDemoData?: boolean;
@@ -260,6 +312,7 @@ const BASE_SCHEMA_SQL = `
260
312
  bio text not null,
261
313
  followers_count integer not null default 0,
262
314
  following_count integer not null default 0,
315
+ public_metrics_json text not null default '{}',
263
316
  avatar_hue integer not null default 0,
264
317
  avatar_url text,
265
318
  location text,
@@ -443,6 +496,8 @@ const BASE_SCHEMA_SQL = `
443
496
  expanded_handle text,
444
497
  title text,
445
498
  description text,
499
+ image_url text,
500
+ site_name text,
446
501
  error text,
447
502
  source text not null,
448
503
  updated_at text not null
@@ -460,6 +515,52 @@ const BASE_SCHEMA_SQL = `
460
515
  primary key (source_kind, source_id, source_position, short_url)
461
516
  );
462
517
 
518
+ create table if not exists follow_snapshots (
519
+ id text primary key,
520
+ account_id text not null,
521
+ direction text not null,
522
+ source text not null,
523
+ status text not null,
524
+ page_count integer not null default 0,
525
+ result_count integer not null default 0,
526
+ started_at text not null,
527
+ completed_at text not null,
528
+ raw_meta_json text not null default '{}'
529
+ );
530
+
531
+ create table if not exists follow_snapshot_members (
532
+ snapshot_id text not null,
533
+ profile_id text not null,
534
+ external_user_id text not null,
535
+ position integer not null,
536
+ primary key (snapshot_id, profile_id)
537
+ );
538
+
539
+ create table if not exists follow_edges (
540
+ account_id text not null,
541
+ direction text not null,
542
+ profile_id text not null,
543
+ external_user_id text not null,
544
+ source text not null,
545
+ current integer not null default 1,
546
+ first_seen_at text not null,
547
+ last_seen_at text not null,
548
+ ended_at text,
549
+ updated_at text not null,
550
+ primary key (account_id, direction, profile_id)
551
+ );
552
+
553
+ create table if not exists follow_events (
554
+ id text primary key,
555
+ account_id text not null,
556
+ direction text not null,
557
+ profile_id text not null,
558
+ external_user_id text not null,
559
+ kind text not null,
560
+ event_at text not null,
561
+ snapshot_id text not null
562
+ );
563
+
463
564
  create virtual table if not exists tweets_fts using fts5(
464
565
  tweet_id unindexed,
465
566
  text
@@ -483,6 +584,7 @@ const INDEX_SQL = `
483
584
  create index if not exists idx_dm_messages_conversation on dm_messages(conversation_id, created_at asc);
484
585
  create index if not exists idx_profiles_followers on profiles(followers_count desc);
485
586
  create index if not exists idx_profiles_following on profiles(following_count desc);
587
+ create index if not exists idx_profiles_handle on profiles(handle);
486
588
  create index if not exists idx_profile_affiliations_subject on profile_affiliations(subject_profile_id, is_active, last_seen_at desc);
487
589
  create index if not exists idx_profile_affiliations_org on profile_affiliations(organization_profile_id, is_active, last_seen_at desc);
488
590
  create index if not exists idx_profile_snapshots_profile on profile_snapshots(profile_id, last_seen_at desc);
@@ -501,6 +603,10 @@ const INDEX_SQL = `
501
603
  create index if not exists idx_link_occurrences_created on link_occurrences(created_at desc);
502
604
  create index if not exists idx_link_occurrences_account on link_occurrences(account_id, created_at desc);
503
605
  create index if not exists idx_link_occurrences_direction on link_occurrences(direction, created_at desc);
606
+ create index if not exists idx_follow_edges_current on follow_edges(account_id, direction, current, last_seen_at desc);
607
+ create index if not exists idx_follow_edges_profile on follow_edges(profile_id, current);
608
+ create index if not exists idx_follow_snapshots_account on follow_snapshots(account_id, direction, completed_at desc);
609
+ create index if not exists idx_follow_events_account on follow_events(account_id, direction, kind, event_at desc);
504
610
  `;
505
611
 
506
612
  function getColumnNames(db: Database, tableName: string): Set<string> {
@@ -556,6 +662,11 @@ function ensureProfileAvatarColumns(db: Database) {
556
662
  "alter table profiles add column raw_json text not null default '{}'",
557
663
  );
558
664
  }
665
+ if (!columnNames.has("public_metrics_json")) {
666
+ db.exec(
667
+ "alter table profiles add column public_metrics_json text not null default '{}'",
668
+ );
669
+ }
559
670
  }
560
671
 
561
672
  function ensureAccountExternalUserIdColumn(db: Database) {
@@ -683,6 +794,8 @@ function ensureLinkIndexTables(db: Database) {
683
794
  expanded_handle text,
684
795
  title text,
685
796
  description text,
797
+ image_url text,
798
+ site_name text,
686
799
  error text,
687
800
  source text not null,
688
801
  updated_at text not null
@@ -700,6 +813,64 @@ function ensureLinkIndexTables(db: Database) {
700
813
  primary key (source_kind, source_id, source_position, short_url)
701
814
  );
702
815
  `);
816
+
817
+ const urlExpansionColumns = getColumnNames(db, "url_expansions");
818
+ if (!urlExpansionColumns.has("image_url")) {
819
+ db.exec("alter table url_expansions add column image_url text");
820
+ }
821
+ if (!urlExpansionColumns.has("site_name")) {
822
+ db.exec("alter table url_expansions add column site_name text");
823
+ }
824
+ }
825
+
826
+ function ensureFollowGraphTables(db: Database) {
827
+ db.exec(`
828
+ create table if not exists follow_snapshots (
829
+ id text primary key,
830
+ account_id text not null,
831
+ direction text not null,
832
+ source text not null,
833
+ status text not null,
834
+ page_count integer not null default 0,
835
+ result_count integer not null default 0,
836
+ started_at text not null,
837
+ completed_at text not null,
838
+ raw_meta_json text not null default '{}'
839
+ );
840
+
841
+ create table if not exists follow_snapshot_members (
842
+ snapshot_id text not null,
843
+ profile_id text not null,
844
+ external_user_id text not null,
845
+ position integer not null,
846
+ primary key (snapshot_id, profile_id)
847
+ );
848
+
849
+ create table if not exists follow_edges (
850
+ account_id text not null,
851
+ direction text not null,
852
+ profile_id text not null,
853
+ external_user_id text not null,
854
+ source text not null,
855
+ current integer not null default 1,
856
+ first_seen_at text not null,
857
+ last_seen_at text not null,
858
+ ended_at text,
859
+ updated_at text not null,
860
+ primary key (account_id, direction, profile_id)
861
+ );
862
+
863
+ create table if not exists follow_events (
864
+ id text primary key,
865
+ account_id text not null,
866
+ direction text not null,
867
+ profile_id text not null,
868
+ external_user_id text not null,
869
+ kind text not null,
870
+ event_at text not null,
871
+ snapshot_id text not null
872
+ );
873
+ `);
703
874
  }
704
875
 
705
876
  function backfillTweetCollections(db: Database) {
@@ -749,6 +920,17 @@ function ensureSchemaIndexes(db: Database) {
749
920
  db.exec(INDEX_SQL);
750
921
  }
751
922
 
923
+ function ensureDemoData(db: Database) {
924
+ if (demoSeedAttempted) {
925
+ return;
926
+ }
927
+
928
+ seedDemoData(db);
929
+ backfillTweetCollections(db);
930
+ backfillTweetAccountEdges(db);
931
+ demoSeedAttempted = true;
932
+ }
933
+
752
934
  function initDatabase(options: InitDatabaseOptions = {}) {
753
935
  ensureBirdclawDirs();
754
936
 
@@ -766,12 +948,16 @@ function initDatabase(options: InitDatabaseOptions = {}) {
766
948
  ensureProfileBioEntitiesTable(nativeDb);
767
949
  ensureIdentitySearchIndexTable(nativeDb);
768
950
  ensureLinkIndexTables(nativeDb);
951
+ ensureFollowGraphTables(nativeDb);
769
952
  ensureSchemaIndexes(nativeDb);
770
953
  if (options.seedDemoData !== false) {
771
- seedDemoData(nativeDb);
954
+ ensureDemoData(nativeDb);
955
+ } else {
956
+ backfillTweetCollections(nativeDb);
957
+ backfillTweetAccountEdges(nativeDb);
772
958
  }
773
- backfillTweetCollections(nativeDb);
774
- backfillTweetAccountEdges(nativeDb);
959
+ } else if (options.seedDemoData !== false) {
960
+ ensureDemoData(nativeDb);
775
961
  }
776
962
 
777
963
  if (!kyselyDb) {
@@ -799,4 +985,5 @@ export function resetDatabaseForTests() {
799
985
 
800
986
  nativeDb?.close();
801
987
  nativeDb = undefined;
988
+ demoSeedAttempted = false;
802
989
  }