birdclaw 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/CHANGELOG.md +92 -1
  2. package/README.md +75 -5
  3. package/package.json +8 -2
  4. package/scripts/browser-perf.mjs +1 -0
  5. package/scripts/start-test-server.mjs +16 -3
  6. package/src/cli.ts +812 -37
  7. package/src/components/AccountSwitcher.tsx +186 -0
  8. package/src/components/AppNav.tsx +37 -7
  9. package/src/components/AvatarChip.tsx +9 -3
  10. package/src/components/DmWorkspace.tsx +18 -8
  11. package/src/components/LinkPreviewCard.tsx +40 -18
  12. package/src/components/MarkdownViewer.tsx +818 -0
  13. package/src/components/ProfileAnalysisStream.tsx +428 -0
  14. package/src/components/ProfilePreview.tsx +120 -9
  15. package/src/components/SavedTimelineView.tsx +30 -8
  16. package/src/components/SyncNowButton.tsx +60 -25
  17. package/src/components/ThemeSlider.tsx +55 -50
  18. package/src/components/TimelineCard.tsx +240 -92
  19. package/src/components/TimelineRouteFrame.tsx +38 -8
  20. package/src/components/TweetMediaGrid.tsx +87 -38
  21. package/src/components/TweetRichText.tsx +45 -17
  22. package/src/components/account-selection.ts +64 -0
  23. package/src/components/useTimelineRouteData.ts +97 -13
  24. package/src/lib/account-sync-job.ts +666 -0
  25. package/src/lib/actions-transport.ts +216 -146
  26. package/src/lib/api-client.ts +128 -53
  27. package/src/lib/archive-finder.ts +78 -63
  28. package/src/lib/archive-import.ts +1593 -1291
  29. package/src/lib/authored-live.ts +262 -204
  30. package/src/lib/avatar-cache.ts +208 -43
  31. package/src/lib/backup.ts +1536 -954
  32. package/src/lib/bird-actions.ts +139 -57
  33. package/src/lib/bird-command.ts +101 -28
  34. package/src/lib/bird.ts +582 -194
  35. package/src/lib/blocklist.ts +40 -23
  36. package/src/lib/blocks-write.ts +129 -80
  37. package/src/lib/blocks.ts +165 -97
  38. package/src/lib/bookmark-sync-job.ts +250 -160
  39. package/src/lib/config.ts +35 -2
  40. package/src/lib/conversation-surface.ts +79 -48
  41. package/src/lib/data-sources.ts +219 -0
  42. package/src/lib/db.ts +95 -4
  43. package/src/lib/dms-live.ts +720 -66
  44. package/src/lib/effect-runtime.ts +45 -0
  45. package/src/lib/follow-graph.ts +224 -180
  46. package/src/lib/geocoding.ts +296 -0
  47. package/src/lib/http-effect.ts +222 -0
  48. package/src/lib/inbox.ts +74 -43
  49. package/src/lib/link-index.ts +88 -76
  50. package/src/lib/link-insights.ts +24 -0
  51. package/src/lib/link-preview-metadata.ts +472 -52
  52. package/src/lib/location.ts +137 -0
  53. package/src/lib/media-fetch.ts +286 -213
  54. package/src/lib/mention-threads-live.ts +445 -288
  55. package/src/lib/mentions-live.ts +549 -354
  56. package/src/lib/moderation-target.ts +102 -65
  57. package/src/lib/moderation-write.ts +77 -18
  58. package/src/lib/mutes-write.ts +129 -80
  59. package/src/lib/mutes.ts +8 -1
  60. package/src/lib/network-map.ts +382 -0
  61. package/src/lib/openai.ts +84 -53
  62. package/src/lib/period-digest.ts +1317 -0
  63. package/src/lib/profile-affiliation-hydration.ts +93 -54
  64. package/src/lib/profile-analysis.ts +1272 -0
  65. package/src/lib/profile-bio-entities.ts +1 -1
  66. package/src/lib/profile-hydration.ts +124 -72
  67. package/src/lib/profile-replies.ts +60 -43
  68. package/src/lib/profile-resolver.ts +402 -294
  69. package/src/lib/queries.ts +983 -203
  70. package/src/lib/research.ts +165 -120
  71. package/src/lib/search-discussion.ts +1016 -0
  72. package/src/lib/sqlite.ts +1 -0
  73. package/src/lib/timeline-collections-live.ts +204 -167
  74. package/src/lib/timeline-live.ts +325 -51
  75. package/src/lib/tweet-account-edges.ts +2 -0
  76. package/src/lib/tweet-lookup.ts +30 -19
  77. package/src/lib/tweet-render.ts +141 -1
  78. package/src/lib/tweet-search-live.ts +565 -0
  79. package/src/lib/types.ts +75 -3
  80. package/src/lib/ui.ts +31 -8
  81. package/src/lib/url-expansion.ts +226 -55
  82. package/src/lib/url-safety.ts +220 -0
  83. package/src/lib/web-sync.ts +222 -149
  84. package/src/lib/whois.ts +166 -147
  85. package/src/lib/x-web.ts +102 -71
  86. package/src/lib/xurl-rate-limits.ts +267 -0
  87. package/src/lib/xurl.ts +1185 -405
  88. package/src/routeTree.gen.ts +273 -0
  89. package/src/routes/__root.tsx +24 -5
  90. package/src/routes/api/action.tsx +127 -78
  91. package/src/routes/api/avatar.tsx +39 -30
  92. package/src/routes/api/blocks.tsx +26 -23
  93. package/src/routes/api/conversation.tsx +25 -14
  94. package/src/routes/api/data-sources.tsx +24 -0
  95. package/src/routes/api/inbox.tsx +27 -21
  96. package/src/routes/api/link-insights.tsx +31 -25
  97. package/src/routes/api/link-preview.tsx +25 -21
  98. package/src/routes/api/network-map.tsx +55 -0
  99. package/src/routes/api/period-digest.tsx +133 -0
  100. package/src/routes/api/profile-analysis.tsx +152 -0
  101. package/src/routes/api/profile-hydrate.tsx +31 -28
  102. package/src/routes/api/query.tsx +80 -55
  103. package/src/routes/api/search-discussion.tsx +169 -0
  104. package/src/routes/api/status.tsx +18 -10
  105. package/src/routes/api/sync.tsx +75 -29
  106. package/src/routes/api/xurl-rate-limits.tsx +24 -0
  107. package/src/routes/data-sources.tsx +255 -0
  108. package/src/routes/discuss.tsx +419 -0
  109. package/src/routes/dms.tsx +95 -28
  110. package/src/routes/inbox.tsx +32 -19
  111. package/src/routes/network-map.tsx +1035 -0
  112. package/src/routes/profile-analyze.tsx +112 -0
  113. package/src/routes/profiles.$handle.tsx +228 -0
  114. package/src/routes/rate-limits.tsx +309 -0
  115. package/src/routes/today.tsx +455 -0
  116. package/src/styles.css +22 -0
@@ -1,4 +1,6 @@
1
+ import { Effect } from "effect";
1
2
  import { getNativeDb } from "./db";
3
+ import { runEffectPromise, tryPromise } from "./effect-runtime";
2
4
  import type { Database } from "./sqlite";
3
5
  import {
4
6
  normalizeUrlExpansionForIndex,
@@ -246,11 +248,19 @@ function rebuildOccurrences(
246
248
  return { occurrences, entityExpansions };
247
249
  }
248
250
 
249
- async function expandWithConcurrency(
251
+ function expandWithConcurrencyEffect(
250
252
  db: Database,
251
253
  urls: string[],
252
254
  options: LinkBackfillOptions,
253
- ) {
255
+ ): Effect.Effect<
256
+ {
257
+ networkExpansions: number;
258
+ cacheExpansions: number;
259
+ misses: number;
260
+ errors: number;
261
+ },
262
+ unknown
263
+ > {
254
264
  const concurrency = Math.max(
255
265
  1,
256
266
  Math.min(options.concurrency ?? DEFAULT_EXPAND_CONCURRENCY, 64),
@@ -261,62 +271,57 @@ async function expandWithConcurrency(
261
271
  misses: 0,
262
272
  errors: 0,
263
273
  };
264
- let nextIndex = 0;
265
274
 
266
- async function worker() {
267
- for (;;) {
268
- const index = nextIndex++;
269
- const url = urls[index];
270
- if (!url) {
271
- return;
272
- }
273
- const result = (
274
- await expandUrls([url], {
275
+ return Effect.forEach(
276
+ urls,
277
+ (url) =>
278
+ tryPromise(() =>
279
+ expandUrls([url], {
275
280
  refresh: options.refresh,
276
281
  fetchImpl: options.fetchImpl,
277
282
  timeoutMs: options.timeoutMs,
278
- })
279
- )[0]!;
280
- if (result.source === "network") {
281
- counts.networkExpansions++;
282
- } else {
283
- counts.cacheExpansions++;
284
- }
285
- if (result.status === "miss") {
286
- counts.misses++;
287
- }
288
- if (result.status === "error") {
289
- counts.errors++;
290
- }
291
- upsertUrlExpansion(db, normalizeUrlExpansionForIndex(result));
292
- }
293
- }
294
-
295
- await Promise.all(
296
- Array.from({ length: Math.min(concurrency, urls.length) }, () => worker()),
297
- );
298
- return counts;
283
+ }),
284
+ ).pipe(
285
+ Effect.map((results) => {
286
+ const result = results[0]!;
287
+ if (result.source === "network") {
288
+ counts.networkExpansions++;
289
+ } else {
290
+ counts.cacheExpansions++;
291
+ }
292
+ if (result.status === "miss") {
293
+ counts.misses++;
294
+ }
295
+ if (result.status === "error") {
296
+ counts.errors++;
297
+ }
298
+ upsertUrlExpansion(db, normalizeUrlExpansionForIndex(result));
299
+ }),
300
+ ),
301
+ { concurrency },
302
+ ).pipe(Effect.as(counts));
299
303
  }
300
304
 
301
- export async function backfillLinkIndex(
305
+ export function backfillLinkIndexEffect(
302
306
  options: LinkBackfillOptions = {},
303
- ): Promise<LinkBackfillResult> {
304
- const db = getNativeDb({ seedDemoData: false });
305
- const { occurrences, entityExpansions } = rebuildOccurrences(
306
- db,
307
- Boolean(options.includeAllUrls),
308
- options.source,
309
- );
310
- const limit =
311
- typeof options.limit === "number" && Number.isFinite(options.limit)
312
- ? Math.max(0, Math.trunc(options.limit))
313
- : undefined;
314
- const needsExpansionClause = options.refresh
315
- ? "1 = 1"
316
- : "(e.short_url is null or e.status in ('error', 'miss'))";
317
-
318
- const urlsToExpand = db
319
- .prepare(`
307
+ ): Effect.Effect<LinkBackfillResult, unknown> {
308
+ return Effect.gen(function* () {
309
+ const db = getNativeDb({ seedDemoData: false });
310
+ const { occurrences, entityExpansions } = rebuildOccurrences(
311
+ db,
312
+ Boolean(options.includeAllUrls),
313
+ options.source,
314
+ );
315
+ const limit =
316
+ typeof options.limit === "number" && Number.isFinite(options.limit)
317
+ ? Math.max(0, Math.trunc(options.limit))
318
+ : undefined;
319
+ const needsExpansionClause = options.refresh
320
+ ? "1 = 1"
321
+ : "(e.short_url is null or e.status in ('error', 'miss'))";
322
+
323
+ const urlsToExpand = db
324
+ .prepare(`
320
325
  select distinct o.short_url
321
326
  from link_occurrences o
322
327
  left join url_expansions e on e.short_url = o.short_url
@@ -325,43 +330,50 @@ export async function backfillLinkIndex(
325
330
  order by o.short_url
326
331
  ${limit === undefined ? "" : "limit ?"}
327
332
  `)
328
- .all(
329
- ...(options.source ? [options.source] : []),
330
- ...(limit === undefined ? [] : [limit]),
331
- ) as Array<{
332
- short_url: string;
333
- }>;
334
-
335
- const expansionCounts = await expandWithConcurrency(
336
- db,
337
- urlsToExpand.map((row) => row.short_url),
338
- options,
339
- );
333
+ .all(
334
+ ...(options.source ? [options.source] : []),
335
+ ...(limit === undefined ? [] : [limit]),
336
+ ) as Array<{
337
+ short_url: string;
338
+ }>;
339
+
340
+ const expansionCounts = yield* expandWithConcurrencyEffect(
341
+ db,
342
+ urlsToExpand.map((row) => row.short_url),
343
+ options,
344
+ );
340
345
 
341
- const uniqueUrls = db
342
- .prepare(`
346
+ const uniqueUrls = db
347
+ .prepare(`
343
348
  select count(distinct short_url) as count
344
349
  from link_occurrences
345
350
  ${options.source ? "where source_kind = ?" : ""}
346
351
  `)
347
- .get(...(options.source ? [options.source] : [])) as { count: number };
348
- const remaining = db
349
- .prepare(`
352
+ .get(...(options.source ? [options.source] : [])) as { count: number };
353
+ const remaining = db
354
+ .prepare(`
350
355
  select count(distinct o.short_url) as count
351
356
  from link_occurrences o
352
357
  left join url_expansions e on e.short_url = o.short_url
353
358
  where (e.short_url is null or e.status in ('error', 'miss'))
354
359
  ${options.source ? "and o.source_kind = ?" : ""}
355
360
  `)
356
- .get(...(options.source ? [options.source] : [])) as { count: number };
361
+ .get(...(options.source ? [options.source] : [])) as { count: number };
362
+
363
+ return {
364
+ occurrences,
365
+ uniqueUrls: Number(uniqueUrls.count),
366
+ entityExpansions,
367
+ ...expansionCounts,
368
+ remainingUnexpanded: Number(remaining.count),
369
+ };
370
+ });
371
+ }
357
372
 
358
- return {
359
- occurrences,
360
- uniqueUrls: Number(uniqueUrls.count),
361
- entityExpansions,
362
- ...expansionCounts,
363
- remainingUnexpanded: Number(remaining.count),
364
- };
373
+ export function backfillLinkIndex(
374
+ options: LinkBackfillOptions = {},
375
+ ): Promise<LinkBackfillResult> {
376
+ return runEffectPromise(backfillLinkIndexEffect(options));
365
377
  }
366
378
 
367
379
  function likePattern(value: string) {
@@ -635,6 +635,30 @@ export function getLinkInsights(
635
635
  conditions.push("o.source_kind = ?");
636
636
  params.push(source);
637
637
  }
638
+ if (query.account && query.account !== "all") {
639
+ conditions.push(`(
640
+ (o.source_kind = 'dm' and o.account_id = ?)
641
+ or (
642
+ o.source_kind = 'tweet'
643
+ and (
644
+ o.account_id = ?
645
+ or exists (
646
+ select 1
647
+ from tweet_account_edges edge
648
+ where edge.account_id = ?
649
+ and edge.tweet_id = o.source_id
650
+ )
651
+ or exists (
652
+ select 1
653
+ from tweet_collections collection
654
+ where collection.account_id = ?
655
+ and collection.tweet_id = o.source_id
656
+ )
657
+ )
658
+ )
659
+ )`);
660
+ params.push(query.account, query.account, query.account, query.account);
661
+ }
638
662
  if (kind === "videos") {
639
663
  addVideoUrlPrefilter(conditions);
640
664
  }