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
@@ -1,8 +1,12 @@
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";
6
10
  import { tweetEntitiesFromXurl } from "./tweet-render";
7
11
  import type {
8
12
  TweetEntities,
@@ -21,11 +25,6 @@ import {
21
25
  ensureStubProfileForXUser,
22
26
  upsertProfileFromXUser,
23
27
  } from "./x-profile";
24
- import {
25
- getTransportStatus,
26
- listUserTweets,
27
- lookupAuthenticatedUser,
28
- } from "./xurl";
29
28
 
30
29
  export type AuthoredSyncMode = "xurl";
31
30
 
@@ -360,43 +359,6 @@ function getOldestTweetId(tweets: XurlMentionData[]) {
360
359
  }, null);
361
360
  }
362
361
 
363
- function resolveAccount(db: Database, accountId?: string) {
364
- const row = accountId
365
- ? (db
366
- .prepare(
367
- "select id, handle, external_user_id from accounts where id = ?",
368
- )
369
- .get(accountId) as
370
- | { id: string; handle: string; external_user_id: string | null }
371
- | undefined)
372
- : (db
373
- .prepare(
374
- `
375
- select id, handle, external_user_id
376
- from accounts
377
- order by is_default desc, created_at asc
378
- limit 1
379
- `,
380
- )
381
- .get() as
382
- | { id: string; handle: string; external_user_id: string | null }
383
- | undefined);
384
-
385
- if (!row) {
386
- throw new Error(`Unknown account: ${accountId ?? "default"}`);
387
- }
388
-
389
- return {
390
- accountId: row.id,
391
- username: row.handle.replace(/^@/, ""),
392
- externalUserId:
393
- typeof row.external_user_id === "string" &&
394
- row.external_user_id.length > 0
395
- ? row.external_user_id
396
- : undefined,
397
- };
398
- }
399
-
400
362
  function normalizeUsername(value: string) {
401
363
  return value.replace(/^@/, "").trim().toLowerCase();
402
364
  }
@@ -444,12 +406,14 @@ function resolveAuthoredIdentityEffect({
444
406
  db: Database;
445
407
  }) {
446
408
  return Effect.gen(function* () {
447
- const status = yield* tryPromise(() => getTransportStatus());
409
+ const status = yield* liveTransportGateway.xurl.getTransportStatus();
448
410
  if (status.availableTransport !== "xurl") {
449
411
  return yield* Effect.fail(new AuthoredSyncError(status.statusText, 4));
450
412
  }
451
413
 
452
- const resolvedAccount = yield* trySync(() => resolveAccount(db, account));
414
+ const resolvedAccount = yield* trySync(() =>
415
+ resolveLiveSyncAccount(db, account),
416
+ );
453
417
  if (resolvedAccount.externalUserId) {
454
418
  return {
455
419
  ...resolvedAccount,
@@ -458,7 +422,8 @@ function resolveAuthoredIdentityEffect({
458
422
  };
459
423
  }
460
424
 
461
- const authenticated = yield* tryPromise(() => lookupAuthenticatedUser());
425
+ const authenticated =
426
+ yield* liveTransportGateway.xurl.lookupAuthenticatedUser();
462
427
  const authenticatedUser = userFromAuthenticatedPayload(authenticated);
463
428
  if (!authenticatedUser?.id) {
464
429
  return yield* Effect.fail(
@@ -911,7 +876,7 @@ export function syncAuthoredTweetsEffect({
911
876
  archiveSinceSeed ??
912
877
  (untilId ? persistedUntilSinceId : cursor.sinceId) ??
913
878
  null;
914
- let nextToken = usePersistedForward
879
+ const initialToken = usePersistedForward
915
880
  ? cursor.token
916
881
  : usePersistedUntil
917
882
  ? cursor.token
@@ -919,19 +884,20 @@ export function syncAuthoredTweetsEffect({
919
884
  let newestSeenId = usePersistedForward
920
885
  ? maxTweetId(cursor.sinceId, cursor.pendingNewestId)
921
886
  : cursor.sinceId;
922
- const pages: XurlUserTweetsResponse[] = [];
923
887
  const sourceUser = toFallbackUser({
924
888
  userId: identity.userId,
925
889
  username: identity.username,
926
890
  authenticatedUser: identity.authenticatedUser,
927
891
  });
928
- let pageCount = 0;
929
892
 
930
- while (parsedMaxPages === null || pageCount < parsedMaxPages) {
931
- const fetched = yield* tryPromise(() =>
932
- 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, {
933
899
  maxResults: pageLimit,
934
- paginationToken: nextToken,
900
+ paginationToken,
935
901
  excludeRetweets: false,
936
902
  sinceId: effectiveSinceId ?? undefined,
937
903
  untilId,
@@ -942,77 +908,66 @@ export function syncAuthoredTweetsEffect({
942
908
  auth: "oauth2",
943
909
  username: identity.username,
944
910
  }),
945
- ).pipe(
946
- Effect.map((page) => ({ ok: true as const, page })),
947
- Effect.catchAll((error) =>
948
- Effect.succeed({ ok: false as const, error }),
949
- ),
950
- );
951
-
952
- if (!fetched.ok) {
953
- if (pages.length === 0) {
954
- return yield* Effect.fail(fetched.error);
955
- }
956
- const payload = mergePages({
957
- pages,
958
- userId: identity.userId,
959
- nextToken: nextToken ?? null,
960
- });
961
- return buildResult({
962
- accountId: identity.accountId,
911
+ getNextCursor: (page) => page.nextToken,
912
+ persistPage: ({ page, nextCursor: pendingToken }) => {
913
+ const pagePayload = mergePages({
914
+ pages: [page],
963
915
  userId: identity.userId,
964
- effectiveSinceId,
965
- nextSinceId: untilId ? cursor.sinceId : effectiveSinceId,
966
- nextToken: nextToken ?? null,
967
- pageCount,
968
- payload,
969
- partial: true,
970
- error: formatError(fetched.error),
916
+ nextToken: page.nextToken,
971
917
  });
972
- }
973
-
974
- const page = fetched.page;
975
- const pagePayload = mergePages({
976
- pages: [page],
977
- userId: identity.userId,
978
- nextToken: page.nextToken,
979
- });
980
- yield* trySync(() =>
981
- mergeAuthoredPayloadIntoLocalStore({
982
- db,
983
- accountId: identity.accountId,
984
- payload: pagePayload,
985
- sourceUser,
986
- }),
987
- );
988
- pages.push(page);
989
- pageCount += 1;
990
- newestSeenId = maxTweetId(newestSeenId, pagePayload.meta.newest_id);
991
- nextToken = page.nextToken ?? undefined;
992
-
993
- const pendingToken = nextToken;
994
- if (pendingToken && untilId) {
995
- yield* trySync(() =>
996
- writePendingUntilCursor(db, identity.accountId, {
997
- sinceId: cursor.sinceId,
998
- token: pendingToken,
999
- untilId,
1000
- requestedSinceId: effectiveSinceId,
1001
- }),
1002
- );
1003
- } else if (pendingToken) {
1004
- yield* trySync(() =>
1005
- writePendingForwardCursor(db, identity.accountId, {
1006
- sinceId: effectiveSinceId,
1007
- token: pendingToken,
1008
- pendingNewestId: newestSeenId,
918
+ return databaseWriteEffect((writeDb) =>
919
+ mergeAuthoredPayloadIntoLocalStore({
920
+ db: writeDb,
921
+ accountId: identity.accountId,
922
+ payload: pagePayload,
923
+ sourceUser,
1009
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
+ ),
1010
948
  );
1011
- }
1012
-
1013
- if (!nextToken) {
1014
- break;
1015
- }
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
+ });
1016
971
  }
1017
972
 
1018
973
  const capped = Boolean(nextToken);