birdclaw 0.8.2 → 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 (89) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +2 -1
  3. package/src/cli/command-context.ts +17 -0
  4. package/src/cli/register-analysis.ts +500 -0
  5. package/src/cli/register-compose.ts +40 -0
  6. package/src/cli/register-graph.ts +132 -0
  7. package/src/cli/register-inbox.ts +41 -0
  8. package/src/cli/register-storage.ts +106 -0
  9. package/src/cli.ts +30 -750
  10. package/src/components/AccountSwitcher.tsx +7 -15
  11. package/src/components/AvatarChip.tsx +1 -1
  12. package/src/components/AvatarPreload.ts +149 -0
  13. package/src/components/MarkdownCitations.tsx +680 -0
  14. package/src/components/MarkdownViewer.tsx +8 -674
  15. package/src/components/ProfileAnalysisClient.ts +191 -0
  16. package/src/components/ProfileAnalysisStream.tsx +16 -185
  17. package/src/components/ProfilePreview.tsx +2 -0
  18. package/src/components/links-controller.ts +162 -0
  19. package/src/components/links-model.ts +198 -0
  20. package/src/components/network-map-controller.ts +84 -0
  21. package/src/components/network-map-model.ts +255 -0
  22. package/src/components/useTimelineRouteData.ts +105 -235
  23. package/src/lib/analysis-runtime.ts +238 -0
  24. package/src/lib/api-client.ts +16 -215
  25. package/src/lib/api-contracts.ts +328 -0
  26. package/src/lib/archive-import-plan.ts +102 -0
  27. package/src/lib/archive-import.ts +170 -239
  28. package/src/lib/authored-live.ts +75 -120
  29. package/src/lib/backup.ts +335 -424
  30. package/src/lib/blocks-write.ts +30 -26
  31. package/src/lib/blocks.ts +18 -20
  32. package/src/lib/database-metrics.ts +88 -0
  33. package/src/lib/database-migrations.ts +34 -0
  34. package/src/lib/database-schema.ts +312 -0
  35. package/src/lib/database-writer.ts +69 -0
  36. package/src/lib/db.ts +84 -330
  37. package/src/lib/dm-read-model.ts +533 -0
  38. package/src/lib/dms-live.ts +34 -97
  39. package/src/lib/follow-graph.ts +17 -27
  40. package/src/lib/import-repository.ts +138 -0
  41. package/src/lib/inbox.ts +2 -1
  42. package/src/lib/live-sync-engine.ts +209 -0
  43. package/src/lib/live-transport-gateway.ts +128 -0
  44. package/src/lib/mention-threads-live.ts +90 -177
  45. package/src/lib/mentions-export.ts +1 -1
  46. package/src/lib/mentions-live.ts +57 -181
  47. package/src/lib/moderation-target.ts +15 -4
  48. package/src/lib/moderation-write.ts +1 -1
  49. package/src/lib/mutes-write.ts +30 -26
  50. package/src/lib/openai-response-runtime.ts +251 -0
  51. package/src/lib/paginated-sync.ts +93 -0
  52. package/src/lib/period-digest.ts +116 -304
  53. package/src/lib/profile-analysis.ts +36 -110
  54. package/src/lib/queries.ts +6 -2381
  55. package/src/lib/query-actions.ts +437 -0
  56. package/src/lib/query-client.tsx +47 -0
  57. package/src/lib/query-read-model-shared.ts +52 -0
  58. package/src/lib/query-read-models.ts +5 -0
  59. package/src/lib/query-resource.ts +41 -0
  60. package/src/lib/query-status.ts +164 -0
  61. package/src/lib/research.ts +1 -1
  62. package/src/lib/runtime-services.ts +20 -0
  63. package/src/lib/search-discussion.ts +75 -279
  64. package/src/lib/server-runtime-services.ts +30 -0
  65. package/src/lib/sqlite.ts +48 -12
  66. package/src/lib/streaming-ingestion.ts +240 -0
  67. package/src/lib/sync-cache.ts +6 -1
  68. package/src/lib/sync-plan.ts +175 -0
  69. package/src/lib/timeline-collections-live.ts +83 -257
  70. package/src/lib/timeline-live.ts +86 -236
  71. package/src/lib/timeline-read-model.ts +1191 -0
  72. package/src/lib/tweet-repository.ts +156 -0
  73. package/src/lib/tweet-search-live.ts +63 -167
  74. package/src/lib/web-sync.ts +67 -50
  75. package/src/lib/whois.ts +2 -1
  76. package/src/routes/__root.tsx +11 -8
  77. package/src/routes/api/action.tsx +1 -1
  78. package/src/routes/api/conversation.tsx +1 -1
  79. package/src/routes/api/query.tsx +32 -26
  80. package/src/routes/api/status.tsx +6 -4
  81. package/src/routes/api/sync.tsx +5 -2
  82. package/src/routes/blocks.tsx +97 -131
  83. package/src/routes/data-sources.tsx +17 -25
  84. package/src/routes/dms.tsx +167 -184
  85. package/src/routes/inbox.tsx +63 -57
  86. package/src/routes/links.tsx +31 -394
  87. package/src/routes/network-map.tsx +41 -344
  88. package/src/routes/rate-limits.tsx +17 -21
  89. package/src/lib/client-cache.ts +0 -109
package/src/lib/db.ts CHANGED
@@ -4,322 +4,22 @@ import NativeSqliteDatabase, {
4
4
  } from "./sqlite";
5
5
  import { Kysely, SqliteDialect } from "kysely";
6
6
  import { ensureBirdclawDirs, getBirdclawPaths } from "./config";
7
+ import {
8
+ type DatabaseMigration,
9
+ runDatabaseMigrations,
10
+ } from "./database-migrations";
7
11
  import { seedDemoData } from "./seed";
12
+ import {
13
+ type DatabaseConnectionRole,
14
+ recordDatabaseStatement,
15
+ } from "./database-metrics";
8
16
 
9
- export interface AccountsTable {
10
- id: string;
11
- name: string;
12
- handle: string;
13
- external_user_id: string | null;
14
- transport: string;
15
- is_default: number;
16
- created_at: string;
17
- }
18
-
19
- export interface ProfilesTable {
20
- id: string;
21
- handle: string;
22
- display_name: string;
23
- bio: string;
24
- followers_count: number;
25
- following_count: number;
26
- public_metrics_json: string;
27
- avatar_hue: number;
28
- avatar_url: string | null;
29
- location: string | null;
30
- url: string | null;
31
- verified_type: string | null;
32
- entities_json: string;
33
- raw_json: string;
34
- created_at: string;
35
- }
36
-
37
- export interface ProfileAffiliationsTable {
38
- subject_profile_id: string;
39
- organization_profile_id: string;
40
- organization_name: string | null;
41
- organization_handle: string | null;
42
- badge_url: string | null;
43
- url: string | null;
44
- label: string | null;
45
- source: string;
46
- is_active: number;
47
- first_seen_at: string;
48
- last_seen_at: string;
49
- raw_json: string;
50
- updated_at: string;
51
- }
52
-
53
- export interface ProfileSnapshotsTable {
54
- profile_id: string;
55
- snapshot_hash: string;
56
- observed_at: string;
57
- last_seen_at: string;
58
- source: string;
59
- handle: string;
60
- display_name: string;
61
- bio: string;
62
- location: string | null;
63
- url: string | null;
64
- verified_type: string | null;
65
- followers_count: number;
66
- following_count: number;
67
- affiliations_json: string;
68
- raw_json: string;
69
- }
70
-
71
- export interface ProfileBioEntitiesTable {
72
- profile_id: string;
73
- kind: string;
74
- value: string;
75
- source: string;
76
- is_active: number;
77
- first_seen_at: string;
78
- last_seen_at: string;
79
- raw_json: string;
80
- }
81
-
82
- export interface IdentitySearchIndexTable {
83
- profile_id: string;
84
- kind: string;
85
- value: string;
86
- normalized_value: string;
87
- source: string;
88
- weight: number;
89
- updated_at: string;
90
- }
91
-
92
- export interface TweetsTable {
93
- id: string;
94
- account_id: string;
95
- author_profile_id: string;
96
- kind: string;
97
- text: string;
98
- created_at: string;
99
- is_replied: number;
100
- reply_to_id: string | null;
101
- like_count: number;
102
- media_count: number;
103
- bookmarked: number;
104
- liked: number;
105
- entities_json: string;
106
- media_json: string;
107
- quoted_tweet_id: string | null;
108
- }
109
-
110
- export interface TweetCollectionsTable {
111
- account_id: string;
112
- tweet_id: string;
113
- kind: "likes" | "bookmarks";
114
- collected_at: string | null;
115
- source: string;
116
- raw_json: string;
117
- updated_at: string;
118
- }
119
-
120
- export interface TweetAccountEdgesTable {
121
- account_id: string;
122
- tweet_id: string;
123
- kind:
124
- | "home"
125
- | "mention"
126
- | "authored"
127
- | "search"
128
- | "profile"
129
- | "thread_context";
130
- first_seen_at: string;
131
- last_seen_at: string;
132
- seen_count: number;
133
- source: string;
134
- raw_json: string;
135
- updated_at: string;
136
- }
137
-
138
- export interface DmConversationsTable {
139
- id: string;
140
- account_id: string;
141
- participant_profile_id: string;
142
- title: string;
143
- inbox_kind: "accepted" | "request";
144
- last_message_at: string;
145
- unread_count: number;
146
- needs_reply: number;
147
- }
148
-
149
- export interface DmMessagesTable {
150
- id: string;
151
- conversation_id: string;
152
- sender_profile_id: string;
153
- text: string;
154
- created_at: string;
155
- direction: "inbound" | "outbound";
156
- is_replied: number;
157
- media_count: number;
158
- }
159
-
160
- export interface TweetActionsTable {
161
- id: string;
162
- account_id: string;
163
- tweet_id: string | null;
164
- kind: string;
165
- body: string;
166
- created_at: string;
167
- }
168
-
169
- export interface BlocksTable {
170
- account_id: string;
171
- profile_id: string;
172
- source: string;
173
- created_at: string;
174
- }
175
-
176
- export interface MutesTable {
177
- account_id: string;
178
- profile_id: string;
179
- source: string;
180
- created_at: string;
181
- }
182
-
183
- export interface AiScoresTable {
184
- entity_kind: string;
185
- entity_id: string;
186
- model: string;
187
- score: number;
188
- summary: string;
189
- reasoning: string;
190
- updated_at: string;
191
- }
192
-
193
- export interface SyncCacheTable {
194
- cache_key: string;
195
- value_json: string;
196
- updated_at: string;
197
- }
198
-
199
- export interface UrlExpansionsTable {
200
- short_url: string;
201
- expanded_url: string;
202
- final_url: string;
203
- status: string;
204
- expanded_tweet_id: string | null;
205
- expanded_handle: string | null;
206
- title: string | null;
207
- description: string | null;
208
- image_url: string | null;
209
- site_name: string | null;
210
- error: string | null;
211
- source: string;
212
- updated_at: string;
213
- }
214
-
215
- export interface LinkOccurrencesTable {
216
- source_kind: "dm" | "tweet";
217
- source_id: string;
218
- source_position: number;
219
- short_url: string;
220
- account_id: string | null;
221
- conversation_id: string | null;
222
- direction: string | null;
223
- created_at: string;
224
- }
225
-
226
- export interface FollowEdgesTable {
227
- account_id: string;
228
- direction: "followers" | "following";
229
- profile_id: string;
230
- external_user_id: string;
231
- source: string;
232
- current: number;
233
- first_seen_at: string;
234
- last_seen_at: string;
235
- ended_at: string | null;
236
- updated_at: string;
237
- }
238
-
239
- export interface FollowSnapshotsTable {
240
- id: string;
241
- account_id: string;
242
- direction: "followers" | "following";
243
- source: string;
244
- status: "complete" | "incomplete";
245
- page_count: number;
246
- result_count: number;
247
- started_at: string;
248
- completed_at: string;
249
- raw_meta_json: string;
250
- }
251
-
252
- export interface FollowSnapshotMembersTable {
253
- snapshot_id: string;
254
- profile_id: string;
255
- external_user_id: string;
256
- position: number;
257
- }
258
-
259
- export interface FollowEventsTable {
260
- id: string;
261
- account_id: string;
262
- direction: "followers" | "following";
263
- profile_id: string;
264
- external_user_id: string;
265
- kind: "started" | "ended";
266
- event_at: string;
267
- snapshot_id: string;
268
- }
269
-
270
- export interface GeocodedLocationsTable {
271
- normalized_key: string;
272
- original: string;
273
- lat: number;
274
- lng: number;
275
- formatted: string | null;
276
- country_code: string | null;
277
- confidence: number | null;
278
- provider: string;
279
- approx_radius_m: number | null;
280
- bounds_json: string;
281
- components_json: string;
282
- hits: number;
283
- created_at: string;
284
- last_used_at: string;
285
- }
286
-
287
- export interface GeocodedLocationsUnresolvedTable {
288
- normalized_key: string;
289
- original: string;
290
- reason: string;
291
- last_attempted_at: string;
292
- ttl_until: string | null;
293
- }
294
-
295
- export interface BirdclawDatabase {
296
- accounts: AccountsTable;
297
- profiles: ProfilesTable;
298
- profile_affiliations: ProfileAffiliationsTable;
299
- profile_snapshots: ProfileSnapshotsTable;
300
- profile_bio_entities: ProfileBioEntitiesTable;
301
- identity_search_index: IdentitySearchIndexTable;
302
- tweets: TweetsTable;
303
- tweet_collections: TweetCollectionsTable;
304
- tweet_account_edges: TweetAccountEdgesTable;
305
- dm_conversations: DmConversationsTable;
306
- dm_messages: DmMessagesTable;
307
- tweet_actions: TweetActionsTable;
308
- blocks: BlocksTable;
309
- mutes: MutesTable;
310
- ai_scores: AiScoresTable;
311
- sync_cache: SyncCacheTable;
312
- url_expansions: UrlExpansionsTable;
313
- link_occurrences: LinkOccurrencesTable;
314
- follow_edges: FollowEdgesTable;
315
- follow_snapshots: FollowSnapshotsTable;
316
- follow_snapshot_members: FollowSnapshotMembersTable;
317
- follow_events: FollowEventsTable;
318
- geocoded_locations: GeocodedLocationsTable;
319
- geocoded_locations_unresolved: GeocodedLocationsUnresolvedTable;
320
- }
17
+ import type { BirdclawDatabase } from "./database-schema";
18
+ export * from "./database-schema";
321
19
 
322
20
  let nativeDb: Database | undefined;
21
+ let readDbs: Database[] = [];
22
+ let readDbIndex = 0;
323
23
  let kyselyDb: Kysely<BirdclawDatabase> | undefined;
324
24
  let demoSeedAttempted = false;
325
25
 
@@ -1047,6 +747,31 @@ function ensureSchemaIndexes(db: Database) {
1047
747
  db.exec(INDEX_SQL);
1048
748
  }
1049
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
+
1050
775
  function ensureDemoData(db: Database) {
1051
776
  if (demoSeedAttempted) {
1052
777
  return;
@@ -1063,26 +788,14 @@ function initDatabase(options: InitDatabaseOptions = {}) {
1063
788
 
1064
789
  if (!nativeDb) {
1065
790
  const { dbPath } = getBirdclawPaths();
1066
- nativeDb = new NativeSqliteDatabase(dbPath);
1067
- nativeDb.exec(BASE_SCHEMA_SQL);
1068
- ensureAccountExternalUserIdColumn(nativeDb);
1069
- ensureDmConversationInboxColumns(nativeDb);
1070
- ensureTweetMetadataColumns(nativeDb);
1071
- ensureProfileAvatarColumns(nativeDb);
1072
- ensureTweetCollectionsTable(nativeDb);
1073
- ensureTweetAccountEdgesTable(nativeDb);
1074
- ensureProfileAffiliationsTable(nativeDb);
1075
- ensureProfileSnapshotsTable(nativeDb);
1076
- ensureProfileBioEntitiesTable(nativeDb);
1077
- ensureIdentitySearchIndexTable(nativeDb);
1078
- ensureLinkIndexTables(nativeDb);
1079
- ensureFollowGraphTables(nativeDb);
1080
- 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);
1081
797
  if (options.seedDemoData !== false) {
1082
798
  ensureDemoData(nativeDb);
1083
- } else {
1084
- backfillTweetCollections(nativeDb);
1085
- backfillTweetAccountEdges(nativeDb);
1086
799
  }
1087
800
  } else if (options.seedDemoData !== false) {
1088
801
  ensureDemoData(nativeDb);
@@ -1097,11 +810,44 @@ function initDatabase(options: InitDatabaseOptions = {}) {
1097
810
  }
1098
811
  }
1099
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
+
1100
825
  export function getNativeDb(options: InitDatabaseOptions = {}) {
1101
826
  initDatabase(options);
1102
827
  return nativeDb as Database;
1103
828
  }
1104
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
+
1105
851
  export function getDb() {
1106
852
  initDatabase();
1107
853
  return kyselyDb as Kysely<BirdclawDatabase>;
@@ -1110,10 +856,14 @@ export function getDb() {
1110
856
  export async function closeDatabase() {
1111
857
  const db = kyselyDb;
1112
858
  const native = nativeDb;
859
+ const readers = readDbs;
1113
860
  kyselyDb = undefined;
1114
861
  nativeDb = undefined;
862
+ readDbs = [];
863
+ readDbIndex = 0;
1115
864
  demoSeedAttempted = false;
1116
865
 
866
+ for (const reader of readers) reader.close();
1117
867
  if (db) {
1118
868
  await db.destroy();
1119
869
  } else {
@@ -1124,9 +874,13 @@ export async function closeDatabase() {
1124
874
  export function resetDatabaseForTests() {
1125
875
  const db = kyselyDb;
1126
876
  const native = nativeDb;
877
+ const readers = readDbs;
1127
878
  kyselyDb = undefined;
1128
879
  nativeDb = undefined;
880
+ readDbs = [];
881
+ readDbIndex = 0;
1129
882
  demoSeedAttempted = false;
883
+ for (const reader of readers) reader.close();
1130
884
  if (db) {
1131
885
  void db.destroy();
1132
886
  } else {