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,8 +1,10 @@
1
1
  import { mkdirSync, writeFileSync } from "node:fs";
2
2
  import path from "node:path";
3
+ import { Effect } from "effect";
3
4
  import { getNativeDb } from "./db";
5
+ import { runEffectPromise } from "./effect-runtime";
4
6
  import { listTimelineItems } from "./queries";
5
- import { lookupTweetsByIds } from "./tweet-lookup";
7
+ import { lookupTweetsByIdsEffect } from "./tweet-lookup";
6
8
  import { renderTweetMarkdown, renderTweetPlainText } from "./tweet-render";
7
9
  import type { TweetEntities, XurlMentionUser } from "./types";
8
10
 
@@ -78,6 +80,13 @@ interface ResearchRow {
78
80
  thread_depth?: number;
79
81
  }
80
82
 
83
+ function trySync<T>(try_: () => T) {
84
+ return Effect.try({
85
+ try: try_,
86
+ catch: (cause) => cause,
87
+ });
88
+ }
89
+
81
90
  function parseJsonField<T>(value: unknown, fallback: T): T {
82
91
  if (typeof value !== "string" || value.length === 0) {
83
92
  return fallback;
@@ -291,80 +300,89 @@ function getTweetDescendants(
291
300
  return rows.map((row) => toResearchNode(row, "local"));
292
301
  }
293
302
 
294
- async function lookupTweetNode(tweetId: string): Promise<ResearchNode | null> {
295
- const payload = await lookupTweetsByIds([tweetId]);
296
- const tweet = payload.data[0];
297
- if (!tweet) {
298
- return null;
299
- }
300
- const entities = normalizeTweetEntities(tweet.entities);
301
-
302
- const usersById = new Map(
303
- (payload.includes?.users ?? []).map((user: XurlMentionUser) => [
304
- user.id,
305
- user,
306
- ]),
307
- );
308
- const author = usersById.get(tweet.author_id) ?? {
309
- id: tweet.author_id,
310
- username: `user_${tweet.author_id}`,
311
- name: `user_${tweet.author_id}`,
312
- };
303
+ function lookupTweetNodeEffect(
304
+ tweetId: string,
305
+ ): Effect.Effect<ResearchNode | null, unknown> {
306
+ return Effect.gen(function* () {
307
+ const payload = yield* lookupTweetsByIdsEffect([tweetId]);
308
+ const tweet = payload.data[0];
309
+ if (!tweet) {
310
+ return null;
311
+ }
312
+ const entities = normalizeTweetEntities(tweet.entities);
313
313
 
314
- return {
315
- id: tweet.id,
316
- url: `https://x.com/${author.username}/status/${tweet.id}`,
317
- authorHandle: author.username,
318
- authorName: author.name,
319
- createdAt: tweet.created_at,
320
- text: tweet.text,
321
- plainText: renderTweetPlainText(tweet.text, entities),
322
- markdown: renderTweetMarkdown(tweet.text, entities),
323
- likeCount: Number(tweet.public_metrics?.like_count ?? 0),
324
- bookmarked: false,
325
- liked: false,
326
- replyToTweetId:
327
- tweet.referenced_tweets?.find((item) => item.type === "replied_to")?.id ??
328
- null,
329
- quotedTweetId:
330
- tweet.referenced_tweets?.find((item) => item.type === "quoted")?.id ??
331
- null,
332
- threadDepth: 0,
333
- source: "live",
334
- };
314
+ const usersById = new Map(
315
+ (payload.includes?.users ?? []).map((user: XurlMentionUser) => [
316
+ user.id,
317
+ user,
318
+ ]),
319
+ );
320
+ const author = usersById.get(tweet.author_id) ?? {
321
+ id: tweet.author_id,
322
+ username: `user_${tweet.author_id}`,
323
+ name: `user_${tweet.author_id}`,
324
+ };
325
+
326
+ return {
327
+ id: tweet.id,
328
+ url: `https://x.com/${author.username}/status/${tweet.id}`,
329
+ authorHandle: author.username,
330
+ authorName: author.name,
331
+ createdAt: tweet.created_at,
332
+ text: tweet.text,
333
+ plainText: renderTweetPlainText(tweet.text, entities),
334
+ markdown: renderTweetMarkdown(tweet.text, entities),
335
+ likeCount: Number(tweet.public_metrics?.like_count ?? 0),
336
+ bookmarked: false,
337
+ liked: false,
338
+ replyToTweetId:
339
+ tweet.referenced_tweets?.find((item) => item.type === "replied_to")
340
+ ?.id ?? null,
341
+ quotedTweetId:
342
+ tweet.referenced_tweets?.find((item) => item.type === "quoted")?.id ??
343
+ null,
344
+ threadDepth: 0,
345
+ source: "live",
346
+ };
347
+ });
335
348
  }
336
349
 
337
- async function collectAncestorChain(
350
+ function collectAncestorChainEffect(
338
351
  tweetId: string,
339
352
  maxThreadDepth: number,
340
- ): Promise<ResearchNode[]> {
341
- const chain: ResearchNode[] = [];
342
- let currentId: string | undefined = tweetId;
343
- const visited = new Set<string>();
344
- let depth = 0;
345
-
346
- while (currentId && !visited.has(currentId) && depth < maxThreadDepth) {
347
- visited.add(currentId);
348
- const localRow = getTweetRowById(currentId);
349
- if (localRow) {
350
- chain.push(toResearchNode(localRow, "local"));
351
- currentId = localRow.reply_to_id ?? undefined;
353
+ ): Effect.Effect<ResearchNode[], unknown> {
354
+ return Effect.gen(function* () {
355
+ const chain: ResearchNode[] = [];
356
+ let currentId: string | undefined = tweetId;
357
+ const visited = new Set<string>();
358
+ let depth = 0;
359
+
360
+ while (currentId && !visited.has(currentId) && depth < maxThreadDepth) {
361
+ const lookupId: string = currentId;
362
+ visited.add(lookupId);
363
+ const localRow: ResearchRow | null = yield* trySync(() =>
364
+ getTweetRowById(lookupId),
365
+ );
366
+ if (localRow) {
367
+ chain.push(yield* trySync(() => toResearchNode(localRow, "local")));
368
+ currentId = localRow.reply_to_id ?? undefined;
369
+ depth += 1;
370
+ continue;
371
+ }
372
+
373
+ const remoteNode = yield* lookupTweetNodeEffect(lookupId);
374
+ if (!remoteNode) {
375
+ break;
376
+ }
377
+ chain.push(remoteNode);
378
+ currentId = remoteNode.replyToTweetId ?? undefined;
352
379
  depth += 1;
353
- continue;
354
380
  }
355
381
 
356
- const remoteNode = await lookupTweetNode(currentId);
357
- if (!remoteNode) {
358
- break;
359
- }
360
- chain.push(remoteNode);
361
- currentId = remoteNode.replyToTweetId ?? undefined;
362
- depth += 1;
363
- }
364
-
365
- return chain
366
- .reverse()
367
- .map((node, index) => ({ ...node, threadDepth: index }));
382
+ return chain
383
+ .reverse()
384
+ .map((node, index) => ({ ...node, threadDepth: index }));
385
+ });
368
386
  }
369
387
 
370
388
  function dedupeNodes(nodes: ResearchNode[]) {
@@ -533,64 +551,89 @@ function resolveSeedTimelineItems({
533
551
  });
534
552
  }
535
553
 
536
- export async function runResearchMode(
554
+ export function runResearchModeEffect(
537
555
  options: ResearchOptions = {},
538
- ): Promise<ResearchReport> {
539
- const limit = Number.isFinite(options.limit ?? 20)
540
- ? Math.max(1, Math.floor(options.limit ?? 20))
541
- : 20;
542
- const maxThreadDepth = Number.isFinite(options.maxThreadDepth ?? 10)
543
- ? Math.max(1, Math.floor(options.maxThreadDepth ?? 10))
544
- : 10;
545
- const seeds = resolveSeedTimelineItems({
546
- account: options.account,
547
- query: options.query,
548
- limit,
549
- });
550
-
551
- const items: ResearchThread[] = [];
552
- for (const seed of seeds) {
553
- const ancestorChain = await collectAncestorChain(seed.id, maxThreadDepth);
554
- const rootId = ancestorChain[0]?.id ?? seed.id;
555
- const localThread = getTweetDescendants(rootId, maxThreadDepth);
556
- const thread = orderThreadNodes(
557
- rootId,
558
- dedupeNodes([...ancestorChain, ...localThread]),
556
+ ): Effect.Effect<ResearchReport, unknown> {
557
+ return Effect.gen(function* () {
558
+ const limit = yield* trySync(() =>
559
+ Number.isFinite(options.limit ?? 20)
560
+ ? Math.max(1, Math.floor(options.limit ?? 20))
561
+ : 20,
562
+ );
563
+ const maxThreadDepth = yield* trySync(() =>
564
+ Number.isFinite(options.maxThreadDepth ?? 10)
565
+ ? Math.max(1, Math.floor(options.maxThreadDepth ?? 10))
566
+ : 10,
567
+ );
568
+ const seeds = yield* trySync(() =>
569
+ resolveSeedTimelineItems({
570
+ account: options.account,
571
+ query: options.query,
572
+ limit,
573
+ }),
559
574
  );
560
- const links = collectExternalLinks(thread);
561
- const handles = collectHandles(thread);
562
- items.push({
563
- seedTweetId: seed.id,
564
- seedUrl: `https://x.com/${seed.author.handle}/status/${seed.id}`,
565
- seedText: renderTweetPlainText(seed.text, seed.entities),
566
- threadRootId: rootId,
567
- thread,
568
- links,
569
- handles,
570
- });
571
- }
572
575
 
573
- const reportBase = {
574
- query: options.query,
575
- account: options.account,
576
- generatedAt: new Date().toISOString(),
577
- seedCount: seeds.length,
578
- threadCount: items.length,
579
- items,
580
- };
581
- const markdown = renderReportMarkdown(reportBase);
582
- const report: ResearchReport = {
583
- ...reportBase,
584
- markdown,
585
- };
576
+ const items: ResearchThread[] = [];
577
+ for (const seed of seeds) {
578
+ const ancestorChain = yield* collectAncestorChainEffect(
579
+ seed.id,
580
+ maxThreadDepth,
581
+ );
582
+ const rootId = ancestorChain[0]?.id ?? seed.id;
583
+ const localThread = yield* trySync(() =>
584
+ getTweetDescendants(rootId, maxThreadDepth),
585
+ );
586
+ const thread = yield* trySync(() =>
587
+ orderThreadNodes(
588
+ rootId,
589
+ dedupeNodes([...ancestorChain, ...localThread]),
590
+ ),
591
+ );
592
+ const links = yield* trySync(() => collectExternalLinks(thread));
593
+ const handles = yield* trySync(() => collectHandles(thread));
594
+ items.push({
595
+ seedTweetId: seed.id,
596
+ seedUrl: `https://x.com/${seed.author.handle}/status/${seed.id}`,
597
+ seedText: yield* trySync(() =>
598
+ renderTweetPlainText(seed.text, seed.entities),
599
+ ),
600
+ threadRootId: rootId,
601
+ thread,
602
+ links,
603
+ handles,
604
+ });
605
+ }
586
606
 
587
- if (options.outPath) {
588
- const resolved = path.resolve(options.outPath);
589
- mkdirSync(path.dirname(resolved), { recursive: true });
590
- writeFileSync(resolved, markdown, "utf8");
591
- }
607
+ const reportBase = {
608
+ query: options.query,
609
+ account: options.account,
610
+ generatedAt: new Date().toISOString(),
611
+ seedCount: seeds.length,
612
+ threadCount: items.length,
613
+ items,
614
+ };
615
+ const markdown = yield* trySync(() => renderReportMarkdown(reportBase));
616
+ const report: ResearchReport = {
617
+ ...reportBase,
618
+ markdown,
619
+ };
620
+
621
+ if (options.outPath) {
622
+ yield* trySync(() => {
623
+ const resolved = path.resolve(options.outPath ?? "");
624
+ mkdirSync(path.dirname(resolved), { recursive: true });
625
+ writeFileSync(resolved, markdown, "utf8");
626
+ });
627
+ }
592
628
 
593
- return report;
629
+ return report;
630
+ });
631
+ }
632
+
633
+ export function runResearchMode(
634
+ options: ResearchOptions = {},
635
+ ): Promise<ResearchReport> {
636
+ return runEffectPromise(runResearchModeEffect(options));
594
637
  }
595
638
 
596
639
  export const __test__ = {
@@ -602,6 +645,8 @@ export const __test__ = {
602
645
  collectExternalLinks,
603
646
  collectHandles,
604
647
  renderReportMarkdown,
648
+ lookupTweetNodeEffect,
649
+ collectAncestorChainEffect,
605
650
  };
606
651
 
607
652
  export type { ResearchRow };