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
@@ -1,8 +1,13 @@
1
1
  import type { Database } from "./sqlite";
2
2
  import { Effect } from "effect";
3
+ import { databaseWriteEffect } from "./database-writer";
3
4
  import { getNativeDb } from "./db";
4
- import { runEffectPromise, tryPromise } from "./effect-runtime";
5
+ import { runEffectPromise } from "./effect-runtime";
6
+ import { liveTransportGateway } from "./live-transport-gateway";
7
+ import { resolveLiveSyncAccount } from "./live-sync-engine";
5
8
  import { readSyncCache, writeSyncCache } from "./sync-cache";
9
+ import { runSyncPlanEffect } from "./sync-plan";
10
+ import { tweetEntitiesFromXurl } from "./tweet-render";
6
11
  import type {
7
12
  TweetEntities,
8
13
  TweetMediaItem,
@@ -20,11 +25,6 @@ import {
20
25
  ensureStubProfileForXUser,
21
26
  upsertProfileFromXUser,
22
27
  } from "./x-profile";
23
- import {
24
- getTransportStatus,
25
- listUserTweets,
26
- lookupAuthenticatedUser,
27
- } from "./xurl";
28
28
 
29
29
  export type AuthoredSyncMode = "xurl";
30
30
 
@@ -359,43 +359,6 @@ function getOldestTweetId(tweets: XurlMentionData[]) {
359
359
  }, null);
360
360
  }
361
361
 
362
- function resolveAccount(db: Database, accountId?: string) {
363
- const row = accountId
364
- ? (db
365
- .prepare(
366
- "select id, handle, external_user_id from accounts where id = ?",
367
- )
368
- .get(accountId) as
369
- | { id: string; handle: string; external_user_id: string | null }
370
- | undefined)
371
- : (db
372
- .prepare(
373
- `
374
- select id, handle, external_user_id
375
- from accounts
376
- order by is_default desc, created_at asc
377
- limit 1
378
- `,
379
- )
380
- .get() as
381
- | { id: string; handle: string; external_user_id: string | null }
382
- | undefined);
383
-
384
- if (!row) {
385
- throw new Error(`Unknown account: ${accountId ?? "default"}`);
386
- }
387
-
388
- return {
389
- accountId: row.id,
390
- username: row.handle.replace(/^@/, ""),
391
- externalUserId:
392
- typeof row.external_user_id === "string" &&
393
- row.external_user_id.length > 0
394
- ? row.external_user_id
395
- : undefined,
396
- };
397
- }
398
-
399
362
  function normalizeUsername(value: string) {
400
363
  return value.replace(/^@/, "").trim().toLowerCase();
401
364
  }
@@ -443,12 +406,14 @@ function resolveAuthoredIdentityEffect({
443
406
  db: Database;
444
407
  }) {
445
408
  return Effect.gen(function* () {
446
- const status = yield* tryPromise(() => getTransportStatus());
409
+ const status = yield* liveTransportGateway.xurl.getTransportStatus();
447
410
  if (status.availableTransport !== "xurl") {
448
411
  return yield* Effect.fail(new AuthoredSyncError(status.statusText, 4));
449
412
  }
450
413
 
451
- const resolvedAccount = yield* trySync(() => resolveAccount(db, account));
414
+ const resolvedAccount = yield* trySync(() =>
415
+ resolveLiveSyncAccount(db, account),
416
+ );
452
417
  if (resolvedAccount.externalUserId) {
453
418
  return {
454
419
  ...resolvedAccount,
@@ -457,7 +422,8 @@ function resolveAuthoredIdentityEffect({
457
422
  };
458
423
  }
459
424
 
460
- const authenticated = yield* tryPromise(() => lookupAuthenticatedUser());
425
+ const authenticated =
426
+ yield* liveTransportGateway.xurl.lookupAuthenticatedUser();
461
427
  const authenticatedUser = userFromAuthenticatedPayload(authenticated);
462
428
  if (!authenticatedUser?.id) {
463
429
  return yield* Effect.fail(
@@ -523,68 +489,7 @@ function toMentionData(tweet: XurlUserTweet, fallbackAuthorId: string) {
523
489
  }
524
490
 
525
491
  function toLocalEntities(tweet: XurlMentionData): TweetEntities {
526
- const raw = tweet.entities;
527
- if (!raw || typeof raw !== "object") {
528
- return {};
529
- }
530
-
531
- const entities = raw as Record<string, unknown>;
532
- const rawMentions = Array.isArray(entities.mentions) ? entities.mentions : [];
533
- const rawUrls = Array.isArray(entities.urls) ? entities.urls : [];
534
- const rawHashtags = Array.isArray(entities.hashtags) ? entities.hashtags : [];
535
-
536
- return {
537
- ...(rawMentions.length
538
- ? {
539
- mentions: rawMentions.map((mention) => {
540
- const value =
541
- mention && typeof mention === "object"
542
- ? (mention as Record<string, unknown>)
543
- : {};
544
- return {
545
- username: String(value.username ?? ""),
546
- id: typeof value.id === "string" ? String(value.id) : undefined,
547
- start: Number(value.start ?? 0),
548
- end: Number(value.end ?? 0),
549
- };
550
- }),
551
- }
552
- : {}),
553
- ...(rawUrls.length
554
- ? {
555
- urls: rawUrls.map((url) => {
556
- const value =
557
- url && typeof url === "object"
558
- ? (url as Record<string, unknown>)
559
- : {};
560
- return {
561
- url: String(value.url ?? ""),
562
- expandedUrl: String(value.expanded_url ?? value.url ?? ""),
563
- displayUrl: String(
564
- value.display_url ?? value.expanded_url ?? value.url ?? "",
565
- ),
566
- start: Number(value.start ?? 0),
567
- end: Number(value.end ?? 0),
568
- };
569
- }),
570
- }
571
- : {}),
572
- ...(rawHashtags.length
573
- ? {
574
- hashtags: rawHashtags.map((hashtag) => {
575
- const value =
576
- hashtag && typeof hashtag === "object"
577
- ? (hashtag as Record<string, unknown>)
578
- : {};
579
- return {
580
- tag: String(value.tag ?? ""),
581
- start: Number(value.start ?? 0),
582
- end: Number(value.end ?? 0),
583
- };
584
- }),
585
- }
586
- : {}),
587
- };
492
+ return tweetEntitiesFromXurl(tweet.entities);
588
493
  }
589
494
 
590
495
  function toMediaType(type: string): TweetMediaItem["type"] {
@@ -971,7 +876,7 @@ export function syncAuthoredTweetsEffect({
971
876
  archiveSinceSeed ??
972
877
  (untilId ? persistedUntilSinceId : cursor.sinceId) ??
973
878
  null;
974
- let nextToken = usePersistedForward
879
+ const initialToken = usePersistedForward
975
880
  ? cursor.token
976
881
  : usePersistedUntil
977
882
  ? cursor.token
@@ -979,19 +884,20 @@ export function syncAuthoredTweetsEffect({
979
884
  let newestSeenId = usePersistedForward
980
885
  ? maxTweetId(cursor.sinceId, cursor.pendingNewestId)
981
886
  : cursor.sinceId;
982
- const pages: XurlUserTweetsResponse[] = [];
983
887
  const sourceUser = toFallbackUser({
984
888
  userId: identity.userId,
985
889
  username: identity.username,
986
890
  authenticatedUser: identity.authenticatedUser,
987
891
  });
988
- let pageCount = 0;
989
892
 
990
- while (parsedMaxPages === null || pageCount < parsedMaxPages) {
991
- const fetched = yield* tryPromise(() =>
992
- listUserTweets(identity.userId, {
893
+ const planResult = yield* runSyncPlanEffect({
894
+ allowPartialFailure: true,
895
+ initialCursor: initialToken,
896
+ maxPages: parsedMaxPages ?? undefined,
897
+ fetchPage: ({ cursor: paginationToken }) =>
898
+ liveTransportGateway.xurl.listUserTweets(identity.userId, {
993
899
  maxResults: pageLimit,
994
- paginationToken: nextToken,
900
+ paginationToken,
995
901
  excludeRetweets: false,
996
902
  sinceId: effectiveSinceId ?? undefined,
997
903
  untilId,
@@ -1002,77 +908,66 @@ export function syncAuthoredTweetsEffect({
1002
908
  auth: "oauth2",
1003
909
  username: identity.username,
1004
910
  }),
1005
- ).pipe(
1006
- Effect.map((page) => ({ ok: true as const, page })),
1007
- Effect.catchAll((error) =>
1008
- Effect.succeed({ ok: false as const, error }),
1009
- ),
1010
- );
1011
-
1012
- if (!fetched.ok) {
1013
- if (pages.length === 0) {
1014
- return yield* Effect.fail(fetched.error);
1015
- }
1016
- const payload = mergePages({
1017
- pages,
1018
- userId: identity.userId,
1019
- nextToken: nextToken ?? null,
1020
- });
1021
- return buildResult({
1022
- accountId: identity.accountId,
911
+ getNextCursor: (page) => page.nextToken,
912
+ persistPage: ({ page, nextCursor: pendingToken }) => {
913
+ const pagePayload = mergePages({
914
+ pages: [page],
1023
915
  userId: identity.userId,
1024
- effectiveSinceId,
1025
- nextSinceId: untilId ? cursor.sinceId : effectiveSinceId,
1026
- nextToken: nextToken ?? null,
1027
- pageCount,
1028
- payload,
1029
- partial: true,
1030
- error: formatError(fetched.error),
916
+ nextToken: page.nextToken,
1031
917
  });
1032
- }
1033
-
1034
- const page = fetched.page;
1035
- const pagePayload = mergePages({
1036
- pages: [page],
1037
- userId: identity.userId,
1038
- nextToken: page.nextToken,
1039
- });
1040
- yield* trySync(() =>
1041
- mergeAuthoredPayloadIntoLocalStore({
1042
- db,
1043
- accountId: identity.accountId,
1044
- payload: pagePayload,
1045
- sourceUser,
1046
- }),
1047
- );
1048
- pages.push(page);
1049
- pageCount += 1;
1050
- newestSeenId = maxTweetId(newestSeenId, pagePayload.meta.newest_id);
1051
- nextToken = page.nextToken ?? undefined;
1052
-
1053
- const pendingToken = nextToken;
1054
- if (pendingToken && untilId) {
1055
- yield* trySync(() =>
1056
- writePendingUntilCursor(db, identity.accountId, {
1057
- sinceId: cursor.sinceId,
1058
- token: pendingToken,
1059
- untilId,
1060
- requestedSinceId: effectiveSinceId,
918
+ return databaseWriteEffect((writeDb) =>
919
+ mergeAuthoredPayloadIntoLocalStore({
920
+ db: writeDb,
921
+ accountId: identity.accountId,
922
+ payload: pagePayload,
923
+ sourceUser,
1061
924
  }),
925
+ ).pipe(
926
+ Effect.flatMap(() =>
927
+ trySync(() => {
928
+ newestSeenId = maxTweetId(
929
+ newestSeenId,
930
+ pagePayload.meta.newest_id,
931
+ );
932
+ if (pendingToken && untilId) {
933
+ writePendingUntilCursor(db, identity.accountId, {
934
+ sinceId: cursor.sinceId,
935
+ token: pendingToken,
936
+ untilId,
937
+ requestedSinceId: effectiveSinceId,
938
+ });
939
+ } else if (pendingToken) {
940
+ writePendingForwardCursor(db, identity.accountId, {
941
+ sinceId: effectiveSinceId,
942
+ token: pendingToken,
943
+ pendingNewestId: newestSeenId,
944
+ });
945
+ }
946
+ }),
947
+ ),
1062
948
  );
1063
- } else if (pendingToken) {
1064
- yield* trySync(() =>
1065
- writePendingForwardCursor(db, identity.accountId, {
1066
- sinceId: effectiveSinceId,
1067
- token: pendingToken,
1068
- pendingNewestId: newestSeenId,
1069
- }),
1070
- );
1071
- }
1072
-
1073
- if (!nextToken) {
1074
- break;
1075
- }
949
+ },
950
+ });
951
+ const pages = planResult.pages;
952
+ const pageCount = pages.length;
953
+ const nextToken = planResult.nextCursor;
954
+ if (planResult.stopReason === "error") {
955
+ const payload = mergePages({
956
+ pages,
957
+ userId: identity.userId,
958
+ nextToken: nextToken ?? null,
959
+ });
960
+ return buildResult({
961
+ accountId: identity.accountId,
962
+ userId: identity.userId,
963
+ effectiveSinceId,
964
+ nextSinceId: untilId ? cursor.sinceId : effectiveSinceId,
965
+ nextToken: nextToken ?? null,
966
+ pageCount,
967
+ payload,
968
+ partial: true,
969
+ error: formatError(planResult.error),
970
+ });
1076
971
  }
1077
972
 
1078
973
  const capped = Boolean(nextToken);