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
@@ -3,6 +3,8 @@ import { existsSync, promises as fs } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import path from "node:path";
5
5
  import { promisify } from "node:util";
6
+ import { Effect } from "effect";
7
+ import { runEffectPromise, tryPromise } from "./effect-runtime";
6
8
  import type { ArchiveCandidate } from "./types";
7
9
 
8
10
  const execAsync = promisify(exec);
@@ -37,11 +39,9 @@ function formatRelativeDate(date: Date): string {
37
39
  return `${Math.floor(days / 365)} years ago`;
38
40
  }
39
41
 
40
- async function getCandidate(
41
- filePath: string,
42
- ): Promise<ArchiveCandidate | null> {
43
- try {
44
- const stats = await fs.stat(filePath);
42
+ function getCandidateEffect(filePath: string) {
43
+ return Effect.gen(function* () {
44
+ const stats = yield* tryPromise(() => fs.stat(filePath));
45
45
  if (!stats.isFile() || stats.size < 1024 * 1024) {
46
46
  return null;
47
47
  }
@@ -54,79 +54,94 @@ async function getCandidate(
54
54
  modifiedTime: stats.mtime.toISOString(),
55
55
  dateFormatted: formatRelativeDate(stats.mtime),
56
56
  };
57
- } catch {
58
- return null;
59
- }
57
+ }).pipe(Effect.catchAll(() => Effect.succeed(null)));
60
58
  }
61
59
 
62
- async function searchDirectory(
63
- directoryPath: string,
64
- ): Promise<ArchiveCandidate[]> {
60
+ function searchDirectoryEffect(directoryPath: string) {
65
61
  if (!existsSync(directoryPath)) {
66
- return [];
62
+ return Effect.succeed([]);
67
63
  }
68
64
 
69
- const entries = await fs.readdir(directoryPath);
70
- const matches = entries.filter((entry) =>
71
- ARCHIVE_NAME_PATTERNS.some((pattern) => pattern.test(entry)),
72
- );
65
+ return Effect.gen(function* () {
66
+ const entries = yield* tryPromise(() => fs.readdir(directoryPath));
67
+ const matches = entries.filter((entry) =>
68
+ ARCHIVE_NAME_PATTERNS.some((pattern) => pattern.test(entry)),
69
+ );
73
70
 
74
- const candidates = await Promise.all(
75
- matches.map((entry) => getCandidate(path.join(directoryPath, entry))),
76
- );
71
+ const candidates = yield* Effect.forEach(
72
+ matches,
73
+ (entry) => getCandidateEffect(path.join(directoryPath, entry)),
74
+ { concurrency: "unbounded" },
75
+ );
77
76
 
78
- return candidates.filter((item) => item !== null);
77
+ return candidates.filter((item) => item !== null);
78
+ }).pipe(Effect.catchAll(() => Effect.succeed([])));
79
79
  }
80
80
 
81
- export async function findArchives(): Promise<ArchiveCandidate[]> {
81
+ function searchSpotlightEffect(query: string) {
82
+ return Effect.gen(function* () {
83
+ const { stdout } = yield* tryPromise(() =>
84
+ execAsync(`mdfind -onlyin ~ '${query}'`, {
85
+ timeout: 5000,
86
+ }),
87
+ );
88
+ const paths = stdout
89
+ .split("\n")
90
+ .map((item) => item.trim())
91
+ .filter((item) => item.length > 0 && item.endsWith(".zip"));
92
+
93
+ return yield* Effect.forEach(paths, getCandidateEffect, {
94
+ concurrency: "unbounded",
95
+ });
96
+ }).pipe(Effect.catchAll(() => Effect.succeed([])));
97
+ }
98
+
99
+ export function findArchivesEffect(): Effect.Effect<
100
+ ArchiveCandidate[],
101
+ unknown
102
+ > {
82
103
  if (process.platform !== "darwin") {
83
- return [];
104
+ return Effect.succeed([]);
84
105
  }
85
106
 
86
- const found = new Map<string, ArchiveCandidate>();
87
- const downloads = await searchDirectory(path.join(homedir(), "Downloads"));
88
-
89
- for (const candidate of downloads) {
90
- found.set(candidate.path, candidate);
91
- }
107
+ return Effect.gen(function* () {
108
+ const found = new Map<string, ArchiveCandidate>();
109
+ const downloads = yield* searchDirectoryEffect(
110
+ path.join(homedir(), "Downloads"),
111
+ );
92
112
 
93
- const queries = [
94
- 'kMDItemDisplayName == "twitter-*.zip"',
95
- 'kMDItemDisplayName == "x-*.zip"',
96
- 'kMDItemDisplayName == "*archive*.zip" && kMDItemKind == "Zip archive"',
97
- ];
98
-
99
- const spotlightCandidates = await Promise.all(
100
- queries.map(async (query) => {
101
- try {
102
- const { stdout } = await execAsync(`mdfind -onlyin ~ '${query}'`, {
103
- timeout: 5000,
104
- });
105
-
106
- const paths = stdout
107
- .split("\n")
108
- .map((item) => item.trim())
109
- .filter((item) => item.length > 0 && item.endsWith(".zip"));
110
-
111
- return Promise.all(paths.map((filePath) => getCandidate(filePath)));
112
- } catch {
113
- // Best-effort only.
114
- return [];
115
- }
116
- }),
117
- );
113
+ for (const candidate of downloads) {
114
+ found.set(candidate.path, candidate);
115
+ }
118
116
 
119
- for (const candidates of spotlightCandidates) {
120
- for (const candidate of candidates) {
121
- if (candidate) {
122
- found.set(candidate.path, candidate);
117
+ const queries = [
118
+ 'kMDItemDisplayName == "twitter-*.zip"',
119
+ 'kMDItemDisplayName == "x-*.zip"',
120
+ 'kMDItemDisplayName == "*archive*.zip" && kMDItemKind == "Zip archive"',
121
+ ];
122
+
123
+ const spotlightCandidates = yield* Effect.forEach(
124
+ queries,
125
+ searchSpotlightEffect,
126
+ { concurrency: "unbounded" },
127
+ );
128
+
129
+ for (const candidates of spotlightCandidates) {
130
+ for (const candidate of candidates) {
131
+ if (candidate) {
132
+ found.set(candidate.path, candidate);
133
+ }
123
134
  }
124
135
  }
125
- }
126
136
 
127
- return [...found.values()].sort(
128
- (left, right) =>
129
- new Date(right.modifiedTime).getTime() -
130
- new Date(left.modifiedTime).getTime(),
131
- );
137
+ return [...found.values()].sort(
138
+ (left, right) =>
139
+ new Date(right.modifiedTime).getTime() -
140
+ new Date(left.modifiedTime).getTime(),
141
+ );
142
+ });
143
+ }
144
+
145
+ export function findArchives(): Promise<ArchiveCandidate[]> {
146
+ return runEffectPromise(findArchivesEffect());
132
147
  }