birdclaw 0.8.1 → 0.8.3

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 (103) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/package.json +2 -4
  3. package/scripts/browser-perf.mjs +27 -0
  4. package/src/cli/command-context.ts +17 -0
  5. package/src/cli/register-analysis.ts +500 -0
  6. package/src/cli/register-compose.ts +40 -0
  7. package/src/cli/register-graph.ts +132 -0
  8. package/src/cli/register-inbox.ts +41 -0
  9. package/src/cli/register-storage.ts +106 -0
  10. package/src/cli.ts +30 -750
  11. package/src/components/AccountSwitcher.tsx +7 -15
  12. package/src/components/AvatarChip.tsx +1 -1
  13. package/src/components/AvatarPreload.ts +149 -0
  14. package/src/components/ConversationThread.tsx +4 -0
  15. package/src/components/EmbeddedTweetCard.tsx +4 -0
  16. package/src/components/FloatingPreview.tsx +382 -0
  17. package/src/components/MarkdownCitations.tsx +680 -0
  18. package/src/components/MarkdownViewer.tsx +7 -715
  19. package/src/components/ProfileAnalysisClient.ts +191 -0
  20. package/src/components/ProfileAnalysisStream.tsx +16 -185
  21. package/src/components/ProfilePreview.tsx +41 -81
  22. package/src/components/TimelineCard.tsx +18 -2
  23. package/src/components/TweetArticleCard.tsx +66 -0
  24. package/src/components/TweetRichText.tsx +33 -2
  25. package/src/components/links-controller.ts +162 -0
  26. package/src/components/links-model.ts +198 -0
  27. package/src/components/network-map-controller.ts +84 -0
  28. package/src/components/network-map-model.ts +255 -0
  29. package/src/components/useDebouncedValue.ts +12 -0
  30. package/src/components/useTimelineRouteData.ts +142 -170
  31. package/src/lib/analysis-runtime.ts +238 -0
  32. package/src/lib/api-client.ts +16 -100
  33. package/src/lib/api-contracts.ts +328 -0
  34. package/src/lib/archive-finder.ts +38 -0
  35. package/src/lib/archive-import-plan.ts +102 -0
  36. package/src/lib/archive-import.ts +170 -239
  37. package/src/lib/authored-live.ts +77 -182
  38. package/src/lib/backup.ts +335 -424
  39. package/src/lib/bird.ts +32 -1
  40. package/src/lib/blocks-write.ts +30 -26
  41. package/src/lib/blocks.ts +18 -20
  42. package/src/lib/database-metrics.ts +88 -0
  43. package/src/lib/database-migrations.ts +34 -0
  44. package/src/lib/database-schema.ts +312 -0
  45. package/src/lib/database-writer.ts +69 -0
  46. package/src/lib/db.ts +141 -334
  47. package/src/lib/dm-read-model.ts +533 -0
  48. package/src/lib/dms-live.ts +34 -97
  49. package/src/lib/follow-graph.ts +17 -27
  50. package/src/lib/import-repository.ts +138 -0
  51. package/src/lib/inbox.ts +2 -1
  52. package/src/lib/live-sync-engine.ts +209 -0
  53. package/src/lib/live-transport-gateway.ts +128 -0
  54. package/src/lib/mention-threads-live.ts +90 -176
  55. package/src/lib/mentions-export.ts +1 -1
  56. package/src/lib/mentions-live.ts +57 -225
  57. package/src/lib/moderation-target.ts +15 -4
  58. package/src/lib/moderation-write.ts +1 -1
  59. package/src/lib/mutes-write.ts +30 -26
  60. package/src/lib/openai-response-runtime.ts +251 -0
  61. package/src/lib/paginated-sync.ts +93 -0
  62. package/src/lib/period-digest.ts +116 -304
  63. package/src/lib/profile-analysis.ts +37 -111
  64. package/src/lib/queries.ts +6 -2380
  65. package/src/lib/query-actions.ts +437 -0
  66. package/src/lib/query-client.tsx +47 -0
  67. package/src/lib/query-read-model-shared.ts +52 -0
  68. package/src/lib/query-read-models.ts +5 -0
  69. package/src/lib/query-resource.ts +41 -0
  70. package/src/lib/query-status.ts +164 -0
  71. package/src/lib/research.ts +1 -1
  72. package/src/lib/runtime-services.ts +20 -0
  73. package/src/lib/search-discussion.ts +75 -279
  74. package/src/lib/server-runtime-services.ts +30 -0
  75. package/src/lib/sqlite.ts +53 -14
  76. package/src/lib/streaming-ingestion.ts +240 -0
  77. package/src/lib/sync-cache.ts +6 -1
  78. package/src/lib/sync-plan.ts +175 -0
  79. package/src/lib/timeline-collections-live.ts +83 -256
  80. package/src/lib/timeline-live.ts +86 -235
  81. package/src/lib/timeline-read-model.ts +1191 -0
  82. package/src/lib/tweet-render.ts +78 -0
  83. package/src/lib/tweet-repository.ts +156 -0
  84. package/src/lib/tweet-search-live.ts +63 -166
  85. package/src/lib/types.ts +8 -0
  86. package/src/lib/ui.ts +1 -1
  87. package/src/lib/web-sync.ts +67 -50
  88. package/src/lib/whois.ts +2 -1
  89. package/src/router.tsx +1 -1
  90. package/src/routes/__root.tsx +11 -21
  91. package/src/routes/api/action.tsx +1 -1
  92. package/src/routes/api/conversation.tsx +1 -1
  93. package/src/routes/api/query.tsx +32 -26
  94. package/src/routes/api/status.tsx +6 -2
  95. package/src/routes/api/sync.tsx +5 -2
  96. package/src/routes/blocks.tsx +97 -131
  97. package/src/routes/data-sources.tsx +17 -25
  98. package/src/routes/dms.tsx +168 -167
  99. package/src/routes/inbox.tsx +63 -57
  100. package/src/routes/links.tsx +31 -329
  101. package/src/routes/network-map.tsx +41 -344
  102. package/src/routes/rate-limits.tsx +17 -21
  103. package/vite.config.ts +0 -2
package/src/lib/db.ts CHANGED
@@ -1,322 +1,25 @@
1
- import NativeSqliteDatabase, { type Database } from "./sqlite";
1
+ import NativeSqliteDatabase, {
2
+ type Database,
3
+ SQLITE_BUSY_TIMEOUT_MS,
4
+ } from "./sqlite";
2
5
  import { Kysely, SqliteDialect } from "kysely";
3
6
  import { ensureBirdclawDirs, getBirdclawPaths } from "./config";
7
+ import {
8
+ type DatabaseMigration,
9
+ runDatabaseMigrations,
10
+ } from "./database-migrations";
4
11
  import { seedDemoData } from "./seed";
12
+ import {
13
+ type DatabaseConnectionRole,
14
+ recordDatabaseStatement,
15
+ } from "./database-metrics";
5
16
 
6
- export interface AccountsTable {
7
- id: string;
8
- name: string;
9
- handle: string;
10
- external_user_id: string | null;
11
- transport: string;
12
- is_default: number;
13
- created_at: string;
14
- }
15
-
16
- export interface ProfilesTable {
17
- id: string;
18
- handle: string;
19
- display_name: string;
20
- bio: string;
21
- followers_count: number;
22
- following_count: number;
23
- public_metrics_json: string;
24
- avatar_hue: number;
25
- avatar_url: string | null;
26
- location: string | null;
27
- url: string | null;
28
- verified_type: string | null;
29
- entities_json: string;
30
- raw_json: string;
31
- created_at: string;
32
- }
33
-
34
- export interface ProfileAffiliationsTable {
35
- subject_profile_id: string;
36
- organization_profile_id: string;
37
- organization_name: string | null;
38
- organization_handle: string | null;
39
- badge_url: string | null;
40
- url: string | null;
41
- label: string | null;
42
- source: string;
43
- is_active: number;
44
- first_seen_at: string;
45
- last_seen_at: string;
46
- raw_json: string;
47
- updated_at: string;
48
- }
49
-
50
- export interface ProfileSnapshotsTable {
51
- profile_id: string;
52
- snapshot_hash: string;
53
- observed_at: string;
54
- last_seen_at: string;
55
- source: string;
56
- handle: string;
57
- display_name: string;
58
- bio: string;
59
- location: string | null;
60
- url: string | null;
61
- verified_type: string | null;
62
- followers_count: number;
63
- following_count: number;
64
- affiliations_json: string;
65
- raw_json: string;
66
- }
67
-
68
- export interface ProfileBioEntitiesTable {
69
- profile_id: string;
70
- kind: string;
71
- value: string;
72
- source: string;
73
- is_active: number;
74
- first_seen_at: string;
75
- last_seen_at: string;
76
- raw_json: string;
77
- }
78
-
79
- export interface IdentitySearchIndexTable {
80
- profile_id: string;
81
- kind: string;
82
- value: string;
83
- normalized_value: string;
84
- source: string;
85
- weight: number;
86
- updated_at: string;
87
- }
88
-
89
- export interface TweetsTable {
90
- id: string;
91
- account_id: string;
92
- author_profile_id: string;
93
- kind: string;
94
- text: string;
95
- created_at: string;
96
- is_replied: number;
97
- reply_to_id: string | null;
98
- like_count: number;
99
- media_count: number;
100
- bookmarked: number;
101
- liked: number;
102
- entities_json: string;
103
- media_json: string;
104
- quoted_tweet_id: string | null;
105
- }
106
-
107
- export interface TweetCollectionsTable {
108
- account_id: string;
109
- tweet_id: string;
110
- kind: "likes" | "bookmarks";
111
- collected_at: string | null;
112
- source: string;
113
- raw_json: string;
114
- updated_at: string;
115
- }
116
-
117
- export interface TweetAccountEdgesTable {
118
- account_id: string;
119
- tweet_id: string;
120
- kind:
121
- | "home"
122
- | "mention"
123
- | "authored"
124
- | "search"
125
- | "profile"
126
- | "thread_context";
127
- first_seen_at: string;
128
- last_seen_at: string;
129
- seen_count: number;
130
- source: string;
131
- raw_json: string;
132
- updated_at: string;
133
- }
134
-
135
- export interface DmConversationsTable {
136
- id: string;
137
- account_id: string;
138
- participant_profile_id: string;
139
- title: string;
140
- inbox_kind: "accepted" | "request";
141
- last_message_at: string;
142
- unread_count: number;
143
- needs_reply: number;
144
- }
145
-
146
- export interface DmMessagesTable {
147
- id: string;
148
- conversation_id: string;
149
- sender_profile_id: string;
150
- text: string;
151
- created_at: string;
152
- direction: "inbound" | "outbound";
153
- is_replied: number;
154
- media_count: number;
155
- }
156
-
157
- export interface TweetActionsTable {
158
- id: string;
159
- account_id: string;
160
- tweet_id: string | null;
161
- kind: string;
162
- body: string;
163
- created_at: string;
164
- }
165
-
166
- export interface BlocksTable {
167
- account_id: string;
168
- profile_id: string;
169
- source: string;
170
- created_at: string;
171
- }
172
-
173
- export interface MutesTable {
174
- account_id: string;
175
- profile_id: string;
176
- source: string;
177
- created_at: string;
178
- }
179
-
180
- export interface AiScoresTable {
181
- entity_kind: string;
182
- entity_id: string;
183
- model: string;
184
- score: number;
185
- summary: string;
186
- reasoning: string;
187
- updated_at: string;
188
- }
189
-
190
- export interface SyncCacheTable {
191
- cache_key: string;
192
- value_json: string;
193
- updated_at: string;
194
- }
195
-
196
- export interface UrlExpansionsTable {
197
- short_url: string;
198
- expanded_url: string;
199
- final_url: string;
200
- status: string;
201
- expanded_tweet_id: string | null;
202
- expanded_handle: string | null;
203
- title: string | null;
204
- description: string | null;
205
- image_url: string | null;
206
- site_name: string | null;
207
- error: string | null;
208
- source: string;
209
- updated_at: string;
210
- }
211
-
212
- export interface LinkOccurrencesTable {
213
- source_kind: "dm" | "tweet";
214
- source_id: string;
215
- source_position: number;
216
- short_url: string;
217
- account_id: string | null;
218
- conversation_id: string | null;
219
- direction: string | null;
220
- created_at: string;
221
- }
222
-
223
- export interface FollowEdgesTable {
224
- account_id: string;
225
- direction: "followers" | "following";
226
- profile_id: string;
227
- external_user_id: string;
228
- source: string;
229
- current: number;
230
- first_seen_at: string;
231
- last_seen_at: string;
232
- ended_at: string | null;
233
- updated_at: string;
234
- }
235
-
236
- export interface FollowSnapshotsTable {
237
- id: string;
238
- account_id: string;
239
- direction: "followers" | "following";
240
- source: string;
241
- status: "complete" | "incomplete";
242
- page_count: number;
243
- result_count: number;
244
- started_at: string;
245
- completed_at: string;
246
- raw_meta_json: string;
247
- }
248
-
249
- export interface FollowSnapshotMembersTable {
250
- snapshot_id: string;
251
- profile_id: string;
252
- external_user_id: string;
253
- position: number;
254
- }
255
-
256
- export interface FollowEventsTable {
257
- id: string;
258
- account_id: string;
259
- direction: "followers" | "following";
260
- profile_id: string;
261
- external_user_id: string;
262
- kind: "started" | "ended";
263
- event_at: string;
264
- snapshot_id: string;
265
- }
266
-
267
- export interface GeocodedLocationsTable {
268
- normalized_key: string;
269
- original: string;
270
- lat: number;
271
- lng: number;
272
- formatted: string | null;
273
- country_code: string | null;
274
- confidence: number | null;
275
- provider: string;
276
- approx_radius_m: number | null;
277
- bounds_json: string;
278
- components_json: string;
279
- hits: number;
280
- created_at: string;
281
- last_used_at: string;
282
- }
283
-
284
- export interface GeocodedLocationsUnresolvedTable {
285
- normalized_key: string;
286
- original: string;
287
- reason: string;
288
- last_attempted_at: string;
289
- ttl_until: string | null;
290
- }
291
-
292
- export interface BirdclawDatabase {
293
- accounts: AccountsTable;
294
- profiles: ProfilesTable;
295
- profile_affiliations: ProfileAffiliationsTable;
296
- profile_snapshots: ProfileSnapshotsTable;
297
- profile_bio_entities: ProfileBioEntitiesTable;
298
- identity_search_index: IdentitySearchIndexTable;
299
- tweets: TweetsTable;
300
- tweet_collections: TweetCollectionsTable;
301
- tweet_account_edges: TweetAccountEdgesTable;
302
- dm_conversations: DmConversationsTable;
303
- dm_messages: DmMessagesTable;
304
- tweet_actions: TweetActionsTable;
305
- blocks: BlocksTable;
306
- mutes: MutesTable;
307
- ai_scores: AiScoresTable;
308
- sync_cache: SyncCacheTable;
309
- url_expansions: UrlExpansionsTable;
310
- link_occurrences: LinkOccurrencesTable;
311
- follow_edges: FollowEdgesTable;
312
- follow_snapshots: FollowSnapshotsTable;
313
- follow_snapshot_members: FollowSnapshotMembersTable;
314
- follow_events: FollowEventsTable;
315
- geocoded_locations: GeocodedLocationsTable;
316
- geocoded_locations_unresolved: GeocodedLocationsUnresolvedTable;
317
- }
17
+ import type { BirdclawDatabase } from "./database-schema";
18
+ export * from "./database-schema";
318
19
 
319
20
  let nativeDb: Database | undefined;
21
+ let readDbs: Database[] = [];
22
+ let readDbIndex = 0;
320
23
  let kyselyDb: Kysely<BirdclawDatabase> | undefined;
321
24
  let demoSeedAttempted = false;
322
25
 
@@ -326,7 +29,7 @@ export interface InitDatabaseOptions {
326
29
 
327
30
  const BASE_SCHEMA_SQL = `
328
31
  pragma journal_mode = wal;
329
- pragma busy_timeout = 5000;
32
+ pragma busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS};
330
33
  pragma foreign_keys = on;
331
34
 
332
35
  create table if not exists accounts (
@@ -948,6 +651,34 @@ function ensureFollowGraphTables(db: Database) {
948
651
  }
949
652
 
950
653
  function backfillTweetCollections(db: Database) {
654
+ const missingKinds = (
655
+ [
656
+ ["likes", "liked"],
657
+ ["bookmarks", "bookmarked"],
658
+ ] as const
659
+ ).filter(([, column]) =>
660
+ db
661
+ .prepare(
662
+ `
663
+ select 1
664
+ from tweets as tweet
665
+ where tweet.${column} = 1
666
+ and not exists (
667
+ select 1
668
+ from tweet_collections as collection
669
+ where collection.account_id = tweet.account_id
670
+ and collection.tweet_id = tweet.id
671
+ and collection.kind = ?
672
+ )
673
+ limit 1
674
+ `,
675
+ )
676
+ .get(column === "liked" ? "likes" : "bookmarks"),
677
+ );
678
+ if (missingKinds.length === 0) {
679
+ return;
680
+ }
681
+
951
682
  const now = new Date().toISOString();
952
683
  const insert = db.prepare(`
953
684
  insert or ignore into tweet_collections (
@@ -963,12 +694,34 @@ function backfillTweetCollections(db: Database) {
963
694
  `);
964
695
 
965
696
  db.transaction(() => {
966
- insert.run("likes", now, "likes");
967
- insert.run("bookmarks", now, "bookmarks");
697
+ for (const [kind] of missingKinds) {
698
+ insert.run(kind, now, kind);
699
+ }
968
700
  })();
969
701
  }
970
702
 
971
703
  function backfillTweetAccountEdges(db: Database) {
704
+ const missing = db
705
+ .prepare(
706
+ `
707
+ select 1
708
+ from tweets as tweet
709
+ where tweet.kind in ('home', 'mention')
710
+ and not exists (
711
+ select 1
712
+ from tweet_account_edges as edge
713
+ where edge.account_id = tweet.account_id
714
+ and edge.tweet_id = tweet.id
715
+ and edge.kind = tweet.kind
716
+ )
717
+ limit 1
718
+ `,
719
+ )
720
+ .get();
721
+ if (!missing) {
722
+ return;
723
+ }
724
+
972
725
  const now = new Date().toISOString();
973
726
  db.prepare(`
974
727
  insert or ignore into tweet_account_edges (
@@ -994,6 +747,31 @@ function ensureSchemaIndexes(db: Database) {
994
747
  db.exec(INDEX_SQL);
995
748
  }
996
749
 
750
+ const DATABASE_MIGRATIONS: readonly DatabaseMigration[] = [
751
+ {
752
+ version: 1,
753
+ name: "canonical schema baseline",
754
+ up: (db) => {
755
+ db.exec(BASE_SCHEMA_SQL);
756
+ ensureAccountExternalUserIdColumn(db);
757
+ ensureDmConversationInboxColumns(db);
758
+ ensureTweetMetadataColumns(db);
759
+ ensureProfileAvatarColumns(db);
760
+ ensureTweetCollectionsTable(db);
761
+ ensureTweetAccountEdgesTable(db);
762
+ ensureProfileAffiliationsTable(db);
763
+ ensureProfileSnapshotsTable(db);
764
+ ensureProfileBioEntitiesTable(db);
765
+ ensureIdentitySearchIndexTable(db);
766
+ ensureLinkIndexTables(db);
767
+ ensureFollowGraphTables(db);
768
+ ensureSchemaIndexes(db);
769
+ backfillTweetCollections(db);
770
+ backfillTweetAccountEdges(db);
771
+ },
772
+ },
773
+ ];
774
+
997
775
  function ensureDemoData(db: Database) {
998
776
  if (demoSeedAttempted) {
999
777
  return;
@@ -1010,26 +788,14 @@ function initDatabase(options: InitDatabaseOptions = {}) {
1010
788
 
1011
789
  if (!nativeDb) {
1012
790
  const { dbPath } = getBirdclawPaths();
1013
- nativeDb = new NativeSqliteDatabase(dbPath);
1014
- nativeDb.exec(BASE_SCHEMA_SQL);
1015
- ensureAccountExternalUserIdColumn(nativeDb);
1016
- ensureDmConversationInboxColumns(nativeDb);
1017
- ensureTweetMetadataColumns(nativeDb);
1018
- ensureProfileAvatarColumns(nativeDb);
1019
- ensureTweetCollectionsTable(nativeDb);
1020
- ensureTweetAccountEdgesTable(nativeDb);
1021
- ensureProfileAffiliationsTable(nativeDb);
1022
- ensureProfileSnapshotsTable(nativeDb);
1023
- ensureProfileBioEntitiesTable(nativeDb);
1024
- ensureIdentitySearchIndexTable(nativeDb);
1025
- ensureLinkIndexTables(nativeDb);
1026
- ensureFollowGraphTables(nativeDb);
1027
- ensureSchemaIndexes(nativeDb);
791
+ nativeDb = createDatabaseConnection(dbPath, "writer");
792
+ nativeDb.exec(`
793
+ pragma busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS};
794
+ pragma foreign_keys = on;
795
+ `);
796
+ runDatabaseMigrations(nativeDb, DATABASE_MIGRATIONS);
1028
797
  if (options.seedDemoData !== false) {
1029
798
  ensureDemoData(nativeDb);
1030
- } else {
1031
- backfillTweetCollections(nativeDb);
1032
- backfillTweetAccountEdges(nativeDb);
1033
799
  }
1034
800
  } else if (options.seedDemoData !== false) {
1035
801
  ensureDemoData(nativeDb);
@@ -1044,11 +810,44 @@ function initDatabase(options: InitDatabaseOptions = {}) {
1044
810
  }
1045
811
  }
1046
812
 
813
+ function createDatabaseConnection(
814
+ dbPath: string,
815
+ role: DatabaseConnectionRole,
816
+ options: { readonly?: boolean } = {},
817
+ ) {
818
+ return new NativeSqliteDatabase(dbPath, {
819
+ ...options,
820
+ onStatement: (sql, durationMs) =>
821
+ recordDatabaseStatement(role, sql, durationMs),
822
+ });
823
+ }
824
+
1047
825
  export function getNativeDb(options: InitDatabaseOptions = {}) {
1048
826
  initDatabase(options);
1049
827
  return nativeDb as Database;
1050
828
  }
1051
829
 
830
+ export function getReadDb(options: InitDatabaseOptions = {}) {
831
+ initDatabase(options);
832
+ if (readDbs.length === 0) {
833
+ const { dbPath } = getBirdclawPaths();
834
+ readDbs = Array.from({ length: 2 }, () => {
835
+ const db = createDatabaseConnection(dbPath, "reader", {
836
+ readonly: true,
837
+ });
838
+ db.exec(`
839
+ pragma busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS};
840
+ pragma foreign_keys = on;
841
+ pragma query_only = on;
842
+ `);
843
+ return db;
844
+ });
845
+ }
846
+ const db = readDbs[readDbIndex % readDbs.length] as Database;
847
+ readDbIndex = (readDbIndex + 1) % readDbs.length;
848
+ return db;
849
+ }
850
+
1052
851
  export function getDb() {
1053
852
  initDatabase();
1054
853
  return kyselyDb as Kysely<BirdclawDatabase>;
@@ -1057,10 +856,14 @@ export function getDb() {
1057
856
  export async function closeDatabase() {
1058
857
  const db = kyselyDb;
1059
858
  const native = nativeDb;
859
+ const readers = readDbs;
1060
860
  kyselyDb = undefined;
1061
861
  nativeDb = undefined;
862
+ readDbs = [];
863
+ readDbIndex = 0;
1062
864
  demoSeedAttempted = false;
1063
865
 
866
+ for (const reader of readers) reader.close();
1064
867
  if (db) {
1065
868
  await db.destroy();
1066
869
  } else {
@@ -1071,9 +874,13 @@ export async function closeDatabase() {
1071
874
  export function resetDatabaseForTests() {
1072
875
  const db = kyselyDb;
1073
876
  const native = nativeDb;
877
+ const readers = readDbs;
1074
878
  kyselyDb = undefined;
1075
879
  nativeDb = undefined;
880
+ readDbs = [];
881
+ readDbIndex = 0;
1076
882
  demoSeedAttempted = false;
883
+ for (const reader of readers) reader.close();
1077
884
  if (db) {
1078
885
  void db.destroy();
1079
886
  } else {