birdclaw 0.8.2 → 0.8.4

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 (247) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +10 -8
  3. package/bin/birdclaw.mjs +4 -66
  4. package/dist/cli/birdclaw.js +28165 -0
  5. package/dist/client/assets/AvatarChip-DLoqM5xD.js +1 -0
  6. package/dist/client/assets/ProfilePreview-BejAuYsJ.js +1 -0
  7. package/dist/client/assets/SavedTimelineView-BIS7HKpp.js +1 -0
  8. package/dist/client/assets/SmartTimestamp-BR0-9se2.js +1 -0
  9. package/dist/client/assets/TimelineRouteFrame-CyGPj0eM.js +1 -0
  10. package/dist/client/assets/blocks-7UDKQp8H.js +1 -0
  11. package/dist/client/assets/bookmarks-CbBkMvXk.js +1 -0
  12. package/dist/client/assets/chunk-C0phanGV.js +1 -0
  13. package/dist/client/assets/circle-check-C_Vq3ElD.js +1 -0
  14. package/dist/client/assets/client-stream-contracts-ys3XhWG4.js +5 -0
  15. package/dist/client/assets/createLucideIcon-JV8isFFR.js +1 -0
  16. package/dist/client/assets/data-sources-Cm2bpNvg.js +1 -0
  17. package/dist/client/assets/discuss-DTjaOugw.js +1 -0
  18. package/dist/client/assets/dms-CGd6Eogq.js +1 -0
  19. package/dist/client/assets/inbox-BHlGuOy2.js +1 -0
  20. package/dist/client/assets/index-CEIG7S6s.js +31 -0
  21. package/dist/client/assets/index-bpFrQ5yG.css +1 -0
  22. package/dist/client/assets/likes-cLrcAQem.js +1 -0
  23. package/dist/client/assets/links-CM9QO1LC.js +1 -0
  24. package/dist/client/assets/mapbox-BxTlqXt9.js +2 -0
  25. package/dist/client/assets/mapbox-gl-dgf_hX3a.js +3215 -0
  26. package/dist/client/assets/mentions-CxKCgzIf.js +1 -0
  27. package/dist/client/assets/network-map-B5a3whqj.js +2 -0
  28. package/dist/client/assets/profile-analyze-CRtNEwlb.js +1 -0
  29. package/dist/client/assets/profile-hydration-client-YEtJsNTv.js +1 -0
  30. package/dist/client/assets/profiles._handle-C82_HoKX.js +1 -0
  31. package/dist/client/assets/query-client-fYu2rzRC.js +17 -0
  32. package/dist/client/assets/rate-limits-ScaEwdVK.js +1 -0
  33. package/dist/client/assets/react-dom-DHl6Ug7F.js +1 -0
  34. package/dist/client/assets/react-nBylq1U5.js +1 -0
  35. package/dist/client/assets/repeat-2-xCwHM9hD.js +1 -0
  36. package/dist/client/assets/routes-q7ZaW4En.js +1 -0
  37. package/dist/client/assets/schemas-I9r6F6ya.js +64 -0
  38. package/dist/client/assets/search-Dq5kYsxA.js +1 -0
  39. package/dist/client/assets/sparkles-CQVlvHsZ.js +1 -0
  40. package/dist/client/assets/styles-B8s3tt3r.css +2 -0
  41. package/dist/client/assets/today-CeG1O7Py.js +1 -0
  42. package/dist/client/assets/triangle-alert-CBL7s2pf.js +1 -0
  43. package/dist/client/assets/useTimelineRouteData-1HsqgvUi.js +1 -0
  44. package/dist/client/assets/users-CHbKlqUr.js +1 -0
  45. package/dist/server/assets/AvatarChip-zUDAEmjL.js +105 -0
  46. package/dist/server/assets/FeedState-ntW0Q2GB.js +214 -0
  47. package/dist/server/assets/ProfileAnalysisStream-BFvhVdhn.js +198 -0
  48. package/dist/server/assets/ProfilePreview-C6gAor_j.js +752 -0
  49. package/dist/server/assets/SavedTimelineView-BG1jnBp1.js +64 -0
  50. package/dist/server/assets/SmartTimestamp-DfwE1NGf.js +45 -0
  51. package/dist/server/assets/SyncNowButton-BOn_IBXS.js +76 -0
  52. package/dist/server/assets/TimelineRouteFrame-CU7ilM7U.js +85 -0
  53. package/dist/server/assets/TweetRichText-DhCVv1-6.js +74 -0
  54. package/dist/server/assets/_tanstack-start-manifest_v-C-ZRnG_n.js +148 -0
  55. package/dist/server/assets/account-selection-CTdFGWRk.js +44 -0
  56. package/dist/server/assets/blocks-C1mgGTB3.js +299 -0
  57. package/dist/server/assets/blocks-DSgQghqP.js +17 -0
  58. package/dist/server/assets/bookmarks-BJN8tykY.js +14 -0
  59. package/dist/server/assets/client-stream-contracts-C2xbWVjm.js +769 -0
  60. package/dist/server/assets/data-sources-BnA1Q94p.js +180 -0
  61. package/dist/server/assets/discuss-CJ1dau1j.js +15 -0
  62. package/dist/server/assets/discuss-QTo08NCN.js +338 -0
  63. package/dist/server/assets/dms-BuLj4bp8.js +368 -0
  64. package/dist/server/assets/dms-DAyQrVM2.js +240 -0
  65. package/dist/server/assets/empty-plugin-adapters-DzrEkdBF.js +5 -0
  66. package/dist/server/assets/inbox-CkGOaW1q.js +214 -0
  67. package/dist/server/assets/inbox-DpWd1EzC.js +146 -0
  68. package/dist/server/assets/likes-DBOAtsJU.js +14 -0
  69. package/dist/server/assets/links-DAhSjynu.js +240 -0
  70. package/dist/server/assets/links-DmH5lxHO.js +468 -0
  71. package/dist/server/assets/mentions-BH2MzKtB.js +26 -0
  72. package/dist/server/assets/network-map-D3CzRcC4.js +224 -0
  73. package/dist/server/assets/network-map-D5DoLOwR.js +582 -0
  74. package/dist/server/assets/profile-analyze-BP65PED0.js +90 -0
  75. package/dist/server/assets/profile-analyze-C1NoM0tV.js +9 -0
  76. package/dist/server/assets/profile-hydration-client-GJ4kwJS8.js +133 -0
  77. package/dist/server/assets/profiles._handle-DjwlN_ac.js +184 -0
  78. package/dist/server/assets/profiles._handle-DsLNj2Ca.js +12 -0
  79. package/dist/server/assets/query-client-BdauHBgd.js +923 -0
  80. package/dist/server/assets/rate-limits-VAr2qzYD.js +262 -0
  81. package/dist/server/assets/route-search-BUlQ5lGM.js +117 -0
  82. package/dist/server/assets/router-DqkwSPiM.js +17026 -0
  83. package/dist/server/assets/routes-A9c9ZhwN.js +26 -0
  84. package/dist/server/assets/start-FaDOTmto.js +4 -0
  85. package/dist/server/assets/today-B5hVM57Z.js +17 -0
  86. package/dist/server/assets/today-fvGiZ7ud.js +303 -0
  87. package/dist/server/assets/ui-qbmcMSSX.js +137 -0
  88. package/{src/components/useDebouncedValue.ts → dist/server/assets/useDebouncedValue-DOnA6Xcy.js} +4 -4
  89. package/dist/server/assets/useTimelineRouteData-27cxynxa.js +1101 -0
  90. package/dist/server/server.js +5520 -0
  91. package/package.json +27 -34
  92. package/playwright.config.ts +0 -35
  93. package/scripts/browser-perf.mjs +0 -427
  94. package/scripts/build-docs-site.mjs +0 -967
  95. package/scripts/docs-site-assets.mjs +0 -311
  96. package/scripts/run-vitest.mjs +0 -21
  97. package/scripts/sanitize-node-options.mjs +0 -23
  98. package/scripts/start-test-server.mjs +0 -42
  99. package/src/cli-moderation.ts +0 -210
  100. package/src/cli.ts +0 -2336
  101. package/src/components/AccountSwitcher.tsx +0 -186
  102. package/src/components/AppNav.tsx +0 -118
  103. package/src/components/AvatarChip.tsx +0 -58
  104. package/src/components/BrandMark.tsx +0 -67
  105. package/src/components/ConversationThread.tsx +0 -131
  106. package/src/components/DmWorkspace.tsx +0 -286
  107. package/src/components/EmbeddedTweetCard.tsx +0 -55
  108. package/src/components/FeedState.tsx +0 -147
  109. package/src/components/FloatingPreview.tsx +0 -382
  110. package/src/components/InboxCard.tsx +0 -152
  111. package/src/components/LinkPreviewCard.tsx +0 -292
  112. package/src/components/MarkdownViewer.tsx +0 -777
  113. package/src/components/ProfileAnalysisStream.tsx +0 -428
  114. package/src/components/ProfilePreview.tsx +0 -129
  115. package/src/components/SavedTimelineView.tsx +0 -155
  116. package/src/components/SmartTimestamp.tsx +0 -72
  117. package/src/components/SyncNowButton.tsx +0 -140
  118. package/src/components/ThemeSlider.tsx +0 -80
  119. package/src/components/TimelineCard.tsx +0 -533
  120. package/src/components/TimelineRouteFrame.tsx +0 -186
  121. package/src/components/TweetArticleCard.tsx +0 -66
  122. package/src/components/TweetMediaGrid.tsx +0 -185
  123. package/src/components/TweetRichText.tsx +0 -159
  124. package/src/components/account-selection.ts +0 -64
  125. package/src/components/useTimelineRouteData.ts +0 -323
  126. package/src/lib/account-sync-job.ts +0 -666
  127. package/src/lib/actions-transport.ts +0 -300
  128. package/src/lib/api-client.ts +0 -419
  129. package/src/lib/archive-finder.ts +0 -185
  130. package/src/lib/archive-import.ts +0 -2764
  131. package/src/lib/authored-live.ts +0 -1072
  132. package/src/lib/avatar-cache.ts +0 -349
  133. package/src/lib/backup.ts +0 -2741
  134. package/src/lib/bird-actions.ts +0 -294
  135. package/src/lib/bird-command.ts +0 -130
  136. package/src/lib/bird.ts +0 -1140
  137. package/src/lib/blocklist.ts +0 -136
  138. package/src/lib/blocks-write.ts +0 -167
  139. package/src/lib/blocks.ts +0 -389
  140. package/src/lib/bookmark-sync-job.ts +0 -536
  141. package/src/lib/client-cache.ts +0 -109
  142. package/src/lib/config.ts +0 -224
  143. package/src/lib/conversation-surface.ts +0 -205
  144. package/src/lib/data-sources.ts +0 -219
  145. package/src/lib/db.ts +0 -1135
  146. package/src/lib/dms-live.ts +0 -933
  147. package/src/lib/effect-runtime.ts +0 -45
  148. package/src/lib/follow-graph.ts +0 -1097
  149. package/src/lib/geocoding.ts +0 -296
  150. package/src/lib/http-effect.ts +0 -222
  151. package/src/lib/identity-search-index.ts +0 -369
  152. package/src/lib/inbox.ts +0 -241
  153. package/src/lib/link-index.ts +0 -641
  154. package/src/lib/link-insights.ts +0 -860
  155. package/src/lib/link-preview-metadata.ts +0 -754
  156. package/src/lib/location.ts +0 -137
  157. package/src/lib/media-fetch.ts +0 -860
  158. package/src/lib/media-includes.ts +0 -165
  159. package/src/lib/mention-threads-live.ts +0 -917
  160. package/src/lib/mentions-export.ts +0 -147
  161. package/src/lib/mentions-live.ts +0 -1238
  162. package/src/lib/moderation-target.ts +0 -216
  163. package/src/lib/moderation-write.ts +0 -131
  164. package/src/lib/mutes-write.ts +0 -167
  165. package/src/lib/mutes.ts +0 -84
  166. package/src/lib/ndjson-stream.ts +0 -106
  167. package/src/lib/network-map.ts +0 -382
  168. package/src/lib/openai.ts +0 -117
  169. package/src/lib/period-digest.ts +0 -1552
  170. package/src/lib/present.ts +0 -118
  171. package/src/lib/profile-affiliation-hydration.ts +0 -228
  172. package/src/lib/profile-affiliations.ts +0 -262
  173. package/src/lib/profile-analysis.ts +0 -1272
  174. package/src/lib/profile-bio-entities.ts +0 -318
  175. package/src/lib/profile-history.ts +0 -164
  176. package/src/lib/profile-hydration.ts +0 -180
  177. package/src/lib/profile-replies.ts +0 -77
  178. package/src/lib/profile-resolver.ts +0 -650
  179. package/src/lib/queries.ts +0 -2381
  180. package/src/lib/research.ts +0 -652
  181. package/src/lib/search-discussion.ts +0 -1016
  182. package/src/lib/seed.ts +0 -613
  183. package/src/lib/sqlite.ts +0 -147
  184. package/src/lib/sync-cache.ts +0 -65
  185. package/src/lib/theme-transition.ts +0 -113
  186. package/src/lib/theme.tsx +0 -165
  187. package/src/lib/timeline-collections-live.ts +0 -581
  188. package/src/lib/timeline-live.ts +0 -452
  189. package/src/lib/tweet-account-edges.ts +0 -48
  190. package/src/lib/tweet-lookup.ts +0 -46
  191. package/src/lib/tweet-render.ts +0 -430
  192. package/src/lib/tweet-search-live.ts +0 -566
  193. package/src/lib/types.ts +0 -723
  194. package/src/lib/ui.ts +0 -529
  195. package/src/lib/url-expansion-store.ts +0 -110
  196. package/src/lib/url-expansion.ts +0 -343
  197. package/src/lib/url-safety.ts +0 -220
  198. package/src/lib/web-sync.ts +0 -516
  199. package/src/lib/whois.ts +0 -1079
  200. package/src/lib/x-profile.ts +0 -365
  201. package/src/lib/x-web.ts +0 -199
  202. package/src/lib/xurl-rate-limits.ts +0 -267
  203. package/src/lib/xurl.ts +0 -1677
  204. package/src/routeTree.gen.ts +0 -723
  205. package/src/router.tsx +0 -20
  206. package/src/routes/__root.tsx +0 -79
  207. package/src/routes/api/action.tsx +0 -147
  208. package/src/routes/api/avatar.tsx +0 -50
  209. package/src/routes/api/blocks.tsx +0 -35
  210. package/src/routes/api/conversation.tsx +0 -45
  211. package/src/routes/api/data-sources.tsx +0 -24
  212. package/src/routes/api/inbox.tsx +0 -43
  213. package/src/routes/api/link-insights.tsx +0 -85
  214. package/src/routes/api/link-preview.tsx +0 -47
  215. package/src/routes/api/network-map.tsx +0 -55
  216. package/src/routes/api/period-digest.tsx +0 -101
  217. package/src/routes/api/profile-analysis.tsx +0 -97
  218. package/src/routes/api/profile-hydrate.tsx +0 -54
  219. package/src/routes/api/query.tsx +0 -115
  220. package/src/routes/api/search-discussion.tsx +0 -113
  221. package/src/routes/api/status.tsx +0 -28
  222. package/src/routes/api/sync.tsx +0 -105
  223. package/src/routes/api/xurl-rate-limits.tsx +0 -24
  224. package/src/routes/blocks.tsx +0 -377
  225. package/src/routes/bookmarks.tsx +0 -18
  226. package/src/routes/data-sources.tsx +0 -257
  227. package/src/routes/discuss.tsx +0 -419
  228. package/src/routes/dms.tsx +0 -432
  229. package/src/routes/inbox.tsx +0 -226
  230. package/src/routes/index.tsx +0 -32
  231. package/src/routes/likes.tsx +0 -18
  232. package/src/routes/links.tsx +0 -994
  233. package/src/routes/mentions.tsx +0 -32
  234. package/src/routes/network-map.tsx +0 -1035
  235. package/src/routes/profile-analyze.tsx +0 -112
  236. package/src/routes/profiles.$handle.tsx +0 -228
  237. package/src/routes/rate-limits.tsx +0 -309
  238. package/src/routes/today.tsx +0 -511
  239. package/src/styles.css +0 -257
  240. package/tsconfig.json +0 -29
  241. package/vite.config.ts +0 -29
  242. /package/{public → dist/client}/birdclaw-mark.png +0 -0
  243. /package/{public → dist/client}/favicon.ico +0 -0
  244. /package/{public → dist/client}/logo192.png +0 -0
  245. /package/{public → dist/client}/logo512.png +0 -0
  246. /package/{public → dist/client}/manifest.json +0 -0
  247. /package/{public → dist/client}/robots.txt +0 -0
package/src/lib/bird.ts DELETED
@@ -1,1140 +0,0 @@
1
- import { execFile } from "node:child_process";
2
- import { mkdtempSync, readFileSync, rmSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import { promisify } from "node:util";
6
- import { Effect } from "effect";
7
- import { getBirdCommand } from "./config";
8
- import { runEffectPromise } from "./effect-runtime";
9
- import type {
10
- XurlMentionData,
11
- XurlFollowUsersResponse,
12
- XurlMentionsResponse,
13
- XurlMentionUser,
14
- XurlReferencedTweet,
15
- XurlTweetsResponse,
16
- } from "./types";
17
-
18
- const execFileAsync = promisify(execFile);
19
- const BIRD_JSON_MAX_BUFFER_BYTES = 512 * 1024 * 1024;
20
- const BIRD_STDOUT_REDIRECT_SCRIPT = 'out="$1"; shift; exec "$@" > "$out"';
21
-
22
- interface BirdTweetMedia {
23
- type?: string;
24
- url?: string;
25
- }
26
-
27
- interface BirdTweetAuthor {
28
- username?: string;
29
- name?: string;
30
- }
31
-
32
- interface BirdTweetArticle {
33
- title?: string;
34
- previewText?: string;
35
- coverImageUrl?: string;
36
- }
37
-
38
- interface BirdTweetItem {
39
- id: string;
40
- text: string;
41
- createdAt: string;
42
- replyCount?: number;
43
- retweetCount?: number;
44
- likeCount?: number;
45
- conversationId?: string;
46
- inReplyToStatusId?: string | null;
47
- quotedStatusId?: string | null;
48
- retweetedStatusId?: string | null;
49
- quotedTweet?: { id?: string | null } | null;
50
- retweetedTweet?: { id?: string | null } | null;
51
- author?: BirdTweetAuthor;
52
- authorId?: string;
53
- media?: BirdTweetMedia[];
54
- article?: BirdTweetArticle | null;
55
- }
56
-
57
- export interface BirdDmUser {
58
- id: string;
59
- username?: string;
60
- name?: string;
61
- profileImageUrl?: string;
62
- }
63
-
64
- export interface BirdDmEvent {
65
- id: string;
66
- conversationId?: string;
67
- text: string;
68
- createdAt?: string;
69
- senderId?: string;
70
- recipientId?: string;
71
- sender?: BirdDmUser;
72
- recipient?: BirdDmUser;
73
- inboxKind?: "accepted" | "request";
74
- isMessageRequest?: boolean;
75
- }
76
-
77
- export interface BirdDmConversation {
78
- id: string;
79
- participants: BirdDmUser[];
80
- messages: BirdDmEvent[];
81
- lastMessageAt?: string;
82
- lastMessagePreview?: string;
83
- inboxKind?: "accepted" | "request";
84
- isMessageRequest?: boolean;
85
- }
86
-
87
- export interface BirdDmsResponse {
88
- success: true;
89
- conversations: BirdDmConversation[];
90
- events: BirdDmEvent[];
91
- }
92
-
93
- export interface BirdAuthenticatedAccount {
94
- id?: string;
95
- username: string;
96
- }
97
-
98
- export type BirdDmRequestAction = "accept" | "reject" | "block";
99
-
100
- export type BirdDmMutationResponse =
101
- | {
102
- success: true;
103
- conversationId?: string;
104
- userId?: string;
105
- username?: string;
106
- blockedUserId?: string;
107
- blockedUsername?: string;
108
- }
109
- | {
110
- success: false;
111
- error: string;
112
- };
113
-
114
- interface BirdUserOverviewPayload {
115
- user?: {
116
- id?: string;
117
- username?: string;
118
- name?: string;
119
- description?: string;
120
- location?: string;
121
- url?: string;
122
- verified?: boolean;
123
- verifiedType?: string;
124
- verified_type?: string;
125
- followersCount?: number;
126
- followingCount?: number;
127
- profileImageUrl?: string;
128
- createdAt?: string;
129
- entities?: Record<string, unknown>;
130
- affiliation?: Record<string, unknown>;
131
- };
132
- }
133
-
134
- interface BirdProfilesPayload {
135
- users?: NonNullable<BirdUserOverviewPayload["user"]>[];
136
- errors?: Array<{ target?: string; error?: string }>;
137
- }
138
-
139
- type BirdFollowUsersPayload =
140
- | NonNullable<BirdUserOverviewPayload["user"]>[]
141
- | {
142
- users?: NonNullable<BirdUserOverviewPayload["user"]>[];
143
- nextCursor?: string | null;
144
- };
145
-
146
- function toIsoTimestamp(value: string) {
147
- const parsed = new Date(value);
148
- if (Number.isNaN(parsed.getTime())) {
149
- return value;
150
- }
151
- return parsed.toISOString();
152
- }
153
-
154
- function escapeJsonStringControlChars(value: string) {
155
- let output = "";
156
- let inString = false;
157
- let escaped = false;
158
-
159
- for (const character of value) {
160
- if (!inString) {
161
- output += character;
162
- if (character === '"') {
163
- inString = true;
164
- }
165
- continue;
166
- }
167
-
168
- if (escaped) {
169
- output += character;
170
- escaped = false;
171
- continue;
172
- }
173
-
174
- if (character === "\\") {
175
- output += character;
176
- escaped = true;
177
- continue;
178
- }
179
-
180
- if (character === '"') {
181
- output += character;
182
- inString = false;
183
- continue;
184
- }
185
-
186
- if (character === "\n") {
187
- output += "\\n";
188
- continue;
189
- }
190
- if (character === "\r") {
191
- output += "\\r";
192
- continue;
193
- }
194
- if (character === "\t") {
195
- output += "\\t";
196
- continue;
197
- }
198
- if (character.charCodeAt(0) < 0x20) {
199
- output += `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`;
200
- continue;
201
- }
202
-
203
- output += character;
204
- }
205
-
206
- return output;
207
- }
208
-
209
- function parseBirdJson(stdout: string) {
210
- try {
211
- return JSON.parse(stdout) as unknown;
212
- } catch (error) {
213
- if (!(error instanceof SyntaxError)) {
214
- throw error;
215
- }
216
- return JSON.parse(escapeJsonStringControlChars(stdout)) as unknown;
217
- }
218
- }
219
-
220
- function formatBirdCommandError(error: unknown, birdCommand: string) {
221
- const text = [
222
- error instanceof Error ? error.message : "",
223
- error &&
224
- typeof error === "object" &&
225
- "stderr" in error &&
226
- typeof error.stderr === "string"
227
- ? error.stderr
228
- : "",
229
- error &&
230
- typeof error === "object" &&
231
- "stdout" in error &&
232
- typeof error.stdout === "string"
233
- ? error.stdout
234
- : "",
235
- ].join("\n");
236
- if (
237
- (error instanceof Error &&
238
- "code" in error &&
239
- (error as { code?: unknown }).code === "ENOENT") ||
240
- (/No such file or directory|command not found|cannot execute/i.test(text) &&
241
- text.includes(birdCommand))
242
- ) {
243
- return new Error(
244
- `bird command unavailable: ${birdCommand}\nInstall bird on PATH, set BIRDCLAW_BIRD_COMMAND, or update ~/.birdclaw/config.json mentions.birdCommand.`,
245
- );
246
- }
247
-
248
- return error;
249
- }
250
-
251
- function isUnsupportedBirdOptionError(error: unknown, option: string) {
252
- if (!error || typeof error !== "object") {
253
- return false;
254
- }
255
- const text = [
256
- error instanceof Error ? error.message : "",
257
- "stderr" in error && typeof error.stderr === "string" ? error.stderr : "",
258
- "stdout" in error && typeof error.stdout === "string" ? error.stdout : "",
259
- ].join("\n");
260
- return text.includes(option) && /unknown option|error:/i.test(text);
261
- }
262
-
263
- function makeBirdStdoutTempEffect() {
264
- return Effect.acquireRelease(
265
- Effect.sync(() => {
266
- const tempDir = mkdtempSync(join(tmpdir(), "birdclaw-bird-"));
267
- return { tempDir, stdoutPath: join(tempDir, "stdout.json") };
268
- }),
269
- ({ tempDir }) =>
270
- Effect.sync(() => rmSync(tempDir, { recursive: true, force: true })),
271
- );
272
- }
273
-
274
- export function runBirdJsonCommandEffect(args: string[], timeoutMs?: number) {
275
- return Effect.scoped(
276
- Effect.gen(function* () {
277
- const birdCommand = yield* Effect.try({
278
- try: () => getBirdCommand(),
279
- catch: (error) =>
280
- error instanceof Error ? error : new Error(String(error)),
281
- });
282
- const { stdoutPath } = yield* makeBirdStdoutTempEffect();
283
- yield* Effect.tryPromise({
284
- try: () =>
285
- execFileAsync(
286
- "/bin/bash",
287
- [
288
- "-c",
289
- BIRD_STDOUT_REDIRECT_SCRIPT,
290
- "birdclaw-bird",
291
- stdoutPath,
292
- birdCommand,
293
- ...args,
294
- ],
295
- { maxBuffer: BIRD_JSON_MAX_BUFFER_BYTES, timeout: timeoutMs },
296
- ),
297
- catch: (error) => formatBirdCommandError(error, birdCommand),
298
- });
299
- return yield* Effect.try({
300
- try: () => readFileSync(stdoutPath, "utf8"),
301
- catch: (error) => error,
302
- });
303
- }),
304
- );
305
- }
306
-
307
- function runBirdJsonCommandAllowFailureEffect(
308
- args: string[],
309
- timeoutMs?: number,
310
- ) {
311
- return Effect.scoped(
312
- Effect.gen(function* () {
313
- const birdCommand = yield* Effect.try({
314
- try: () => getBirdCommand(),
315
- catch: (error) =>
316
- error instanceof Error ? error : new Error(String(error)),
317
- });
318
- const { stdoutPath } = yield* makeBirdStdoutTempEffect();
319
- yield* Effect.tryPromise({
320
- try: () =>
321
- execFileAsync(
322
- "/bin/bash",
323
- [
324
- "-c",
325
- BIRD_STDOUT_REDIRECT_SCRIPT,
326
- "birdclaw-bird",
327
- stdoutPath,
328
- birdCommand,
329
- ...args,
330
- ],
331
- { maxBuffer: BIRD_JSON_MAX_BUFFER_BYTES, timeout: timeoutMs },
332
- ).catch((error: unknown) => {
333
- const stdout = readFileSync(stdoutPath, "utf8");
334
- if (stdout.trim().length > 0) {
335
- return { stdout: "", stderr: "" };
336
- }
337
- throw formatBirdCommandError(error, birdCommand);
338
- }),
339
- catch: (error) => error,
340
- });
341
- return yield* Effect.try({
342
- try: () => readFileSync(stdoutPath, "utf8"),
343
- catch: (error) => error,
344
- });
345
- }),
346
- );
347
- }
348
-
349
- function getBirdTweetItems(payload: unknown, command: string) {
350
- if (Array.isArray(payload)) {
351
- return payload as BirdTweetItem[];
352
- }
353
-
354
- if (
355
- payload &&
356
- typeof payload === "object" &&
357
- Array.isArray((payload as { tweets?: unknown }).tweets)
358
- ) {
359
- return (payload as { tweets: BirdTweetItem[] }).tweets;
360
- }
361
-
362
- throw new Error(`bird ${command} returned unexpected JSON`);
363
- }
364
-
365
- function getBirdTweetItem(payload: unknown, command: string) {
366
- if (payload && typeof payload === "object") {
367
- const record = payload as { id?: unknown };
368
- if (typeof record.id === "string" && record.id.length > 0) {
369
- return payload as BirdTweetItem;
370
- }
371
- }
372
-
373
- throw new Error(`bird ${command} returned unexpected JSON`);
374
- }
375
-
376
- function toMediaEntities(media: BirdTweetMedia[] | undefined) {
377
- if (!Array.isArray(media) || media.length === 0) {
378
- return undefined;
379
- }
380
-
381
- return {
382
- urls: media
383
- .filter((item) => typeof item?.url === "string" && item.url.length > 0)
384
- .map((item, index) => ({
385
- start: index,
386
- end: index,
387
- url: item.url as string,
388
- expanded_url: item.url as string,
389
- display_url: item.url as string,
390
- media_key: `bird_media_${index}`,
391
- })),
392
- };
393
- }
394
-
395
- function toTweetEntities(item: BirdTweetItem) {
396
- const mediaEntities = toMediaEntities(item.media);
397
- const title = item.article?.title?.trim();
398
- if (!title) return mediaEntities;
399
- const handle = item.author?.username?.replace(/^@/, "");
400
- const url = handle
401
- ? `https://x.com/${handle}/status/${item.id}`
402
- : `https://x.com/i/status/${item.id}`;
403
- return {
404
- ...mediaEntities,
405
- article: {
406
- title,
407
- url,
408
- ...(item.article?.previewText?.trim()
409
- ? { previewText: item.article.previewText.trim() }
410
- : {}),
411
- ...(item.article?.coverImageUrl?.trim()
412
- ? { coverImageUrl: item.article.coverImageUrl.trim() }
413
- : {}),
414
- },
415
- };
416
- }
417
-
418
- function toReferencedTweets(item: BirdTweetItem) {
419
- const references: XurlReferencedTweet[] = [];
420
- if (typeof item.inReplyToStatusId === "string" && item.inReplyToStatusId) {
421
- references.push({ type: "replied_to", id: item.inReplyToStatusId });
422
- }
423
-
424
- const quotedTweetId =
425
- typeof item.quotedStatusId === "string" && item.quotedStatusId
426
- ? item.quotedStatusId
427
- : typeof item.quotedTweet?.id === "string" && item.quotedTweet.id
428
- ? item.quotedTweet.id
429
- : null;
430
- if (quotedTweetId) {
431
- references.push({ type: "quoted", id: quotedTweetId });
432
- }
433
-
434
- const retweetedTweetId =
435
- typeof item.retweetedStatusId === "string" && item.retweetedStatusId
436
- ? item.retweetedStatusId
437
- : typeof item.retweetedTweet?.id === "string" && item.retweetedTweet.id
438
- ? item.retweetedTweet.id
439
- : null;
440
- if (retweetedTweetId) {
441
- references.push({ type: "retweeted", id: retweetedTweetId });
442
- }
443
-
444
- return references.length > 0 ? references : undefined;
445
- }
446
-
447
- function normalizeBirdTweets(items: BirdTweetItem[]): XurlMentionsResponse {
448
- const users = new Map<string, XurlMentionUser>();
449
- const data = items.map((item): XurlMentionData => {
450
- const authorId = String(
451
- item.authorId ?? item.author?.username ?? "unknown",
452
- );
453
- if (!users.has(authorId)) {
454
- users.set(authorId, {
455
- id: authorId,
456
- username: item.author?.username ?? `user_${authorId}`,
457
- name: item.author?.name ?? item.author?.username ?? `user_${authorId}`,
458
- });
459
- }
460
-
461
- return {
462
- id: item.id,
463
- author_id: authorId,
464
- text: item.text,
465
- created_at: toIsoTimestamp(item.createdAt),
466
- conversation_id: item.conversationId ?? item.id,
467
- entities: toTweetEntities(item),
468
- referenced_tweets: toReferencedTweets(item),
469
- public_metrics: {
470
- reply_count: Number(item.replyCount ?? 0),
471
- retweet_count: Number(item.retweetCount ?? 0),
472
- like_count: Number(item.likeCount ?? 0),
473
- },
474
- edit_history_tweet_ids: [item.id],
475
- };
476
- });
477
-
478
- return {
479
- data,
480
- includes:
481
- users.size > 0 ? { users: Array.from(users.values()) } : undefined,
482
- meta: {
483
- result_count: data.length,
484
- page_count: 1,
485
- next_token: null,
486
- ...(data[0] ? { newest_id: data[0].id } : {}),
487
- ...(data.at(-1) ? { oldest_id: data.at(-1)?.id } : {}),
488
- },
489
- };
490
- }
491
-
492
- function parseBirdJsonEffect(stdout: string) {
493
- return Effect.try({
494
- try: () => parseBirdJson(stdout),
495
- catch: (error) => error,
496
- });
497
- }
498
-
499
- function normalizeBirdTweetsPayloadEffect(payload: unknown, command: string) {
500
- return Effect.try({
501
- try: () => normalizeBirdTweets(getBirdTweetItems(payload, command)),
502
- catch: (error) => error,
503
- });
504
- }
505
-
506
- function normalizeBirdTweetItemEffect(payload: unknown, command: string) {
507
- return Effect.try({
508
- try: () => getBirdTweetItem(payload, command),
509
- catch: (error) => error,
510
- });
511
- }
512
-
513
- export function listMentionsViaBirdEffect({
514
- maxResults,
515
- }: {
516
- maxResults: number;
517
- }): Effect.Effect<XurlMentionsResponse, unknown> {
518
- return Effect.gen(function* () {
519
- const stdout = yield* runBirdJsonCommandEffect([
520
- "mentions",
521
- "-n",
522
- String(maxResults),
523
- "--json",
524
- ]);
525
- const payload = yield* parseBirdJsonEffect(stdout);
526
- return yield* normalizeBirdTweetsPayloadEffect(payload, "mentions");
527
- });
528
- }
529
-
530
- export function listMentionsViaBird(options: {
531
- maxResults: number;
532
- }): Promise<XurlMentionsResponse> {
533
- return runEffectPromise(listMentionsViaBirdEffect(options));
534
- }
535
-
536
- function listTweetsViaBirdCommandEffect({
537
- command,
538
- maxResults,
539
- all,
540
- maxPages,
541
- }: {
542
- command: "likes" | "bookmarks";
543
- maxResults: number;
544
- all?: boolean;
545
- maxPages?: number;
546
- }): Effect.Effect<XurlMentionsResponse, unknown> {
547
- return Effect.gen(function* () {
548
- const args = [command, "-n", String(maxResults), "--json"];
549
- if (all) {
550
- args.push("--all");
551
- }
552
- if (maxPages !== undefined) {
553
- args.push("--max-pages", String(maxPages));
554
- }
555
- const stdout = yield* runBirdJsonCommandEffect(args);
556
- const payload = yield* parseBirdJsonEffect(stdout);
557
- return yield* normalizeBirdTweetsPayloadEffect(payload, command);
558
- });
559
- }
560
-
561
- export function listLikedTweetsViaBirdEffect(options: {
562
- maxResults: number;
563
- all?: boolean;
564
- maxPages?: number;
565
- }): Effect.Effect<XurlMentionsResponse, unknown> {
566
- return listTweetsViaBirdCommandEffect({
567
- command: "likes",
568
- ...options,
569
- });
570
- }
571
-
572
- export function listLikedTweetsViaBird(options: {
573
- maxResults: number;
574
- all?: boolean;
575
- maxPages?: number;
576
- }): Promise<XurlMentionsResponse> {
577
- return runEffectPromise(listLikedTweetsViaBirdEffect(options));
578
- }
579
-
580
- export function listBookmarkedTweetsViaBirdEffect(options: {
581
- maxResults: number;
582
- all?: boolean;
583
- maxPages?: number;
584
- }): Effect.Effect<XurlMentionsResponse, unknown> {
585
- return listTweetsViaBirdCommandEffect({
586
- command: "bookmarks",
587
- ...options,
588
- });
589
- }
590
-
591
- export function listBookmarkedTweetsViaBird(options: {
592
- maxResults: number;
593
- all?: boolean;
594
- maxPages?: number;
595
- }): Promise<XurlMentionsResponse> {
596
- return runEffectPromise(listBookmarkedTweetsViaBirdEffect(options));
597
- }
598
-
599
- export function searchTweetsViaBirdEffect(
600
- query: string,
601
- options: {
602
- maxResults: number;
603
- all?: boolean;
604
- maxPages?: number;
605
- },
606
- ): Effect.Effect<XurlMentionsResponse, unknown> {
607
- return Effect.gen(function* () {
608
- const args = ["search", query, "-n", String(options.maxResults), "--json"];
609
- if (options.all) {
610
- args.push("--all");
611
- }
612
- if (options.all && options.maxPages !== undefined) {
613
- args.push("--max-pages", String(options.maxPages));
614
- }
615
- const stdout = yield* runBirdJsonCommandEffect(args);
616
- const payload = yield* parseBirdJsonEffect(stdout);
617
- return yield* normalizeBirdTweetsPayloadEffect(payload, "search");
618
- });
619
- }
620
-
621
- export function searchTweetsViaBird(
622
- query: string,
623
- options: {
624
- maxResults: number;
625
- all?: boolean;
626
- maxPages?: number;
627
- },
628
- ): Promise<XurlMentionsResponse> {
629
- return runEffectPromise(searchTweetsViaBirdEffect(query, options));
630
- }
631
-
632
- export function lookupTweetsByIdsViaBirdEffect(
633
- ids: string[],
634
- ): Effect.Effect<XurlTweetsResponse, unknown> {
635
- if (ids.length === 0) {
636
- return Effect.succeed({ data: [] });
637
- }
638
-
639
- return Effect.gen(function* () {
640
- const tweets = yield* Effect.forEach(
641
- ids,
642
- (id) =>
643
- Effect.gen(function* () {
644
- const stdout = yield* runBirdJsonCommandEffect([
645
- "read",
646
- id,
647
- "--json",
648
- ]);
649
- const payload = yield* parseBirdJsonEffect(stdout);
650
- return yield* normalizeBirdTweetItemEffect(payload, "read");
651
- }),
652
- { concurrency: "unbounded" },
653
- );
654
- return normalizeBirdTweets(tweets);
655
- });
656
- }
657
-
658
- export function lookupTweetsByIdsViaBird(
659
- ids: string[],
660
- ): Promise<XurlTweetsResponse> {
661
- return runEffectPromise(lookupTweetsByIdsViaBirdEffect(ids));
662
- }
663
-
664
- export function listHomeTimelineViaBirdEffect({
665
- maxResults,
666
- following = true,
667
- }: {
668
- maxResults: number;
669
- following?: boolean;
670
- }): Effect.Effect<XurlMentionsResponse, unknown> {
671
- return Effect.gen(function* () {
672
- const args = ["home", "-n", String(maxResults), "--json"];
673
- if (following) {
674
- args.push("--following");
675
- }
676
- const stdout = yield* runBirdJsonCommandEffect(args);
677
- const payload = yield* parseBirdJsonEffect(stdout);
678
- return yield* normalizeBirdTweetsPayloadEffect(payload, "home");
679
- });
680
- }
681
-
682
- export function listHomeTimelineViaBird(options: {
683
- maxResults: number;
684
- following?: boolean;
685
- }): Promise<XurlMentionsResponse> {
686
- return runEffectPromise(listHomeTimelineViaBirdEffect(options));
687
- }
688
-
689
- function normalizeBirdFollowUsers(
690
- payload: unknown,
691
- command: "followers" | "following",
692
- maxResults: number,
693
- ): XurlFollowUsersResponse {
694
- const rawPayload = payload as BirdFollowUsersPayload;
695
- const users = Array.isArray(rawPayload) ? rawPayload : rawPayload.users;
696
- if (!Array.isArray(users)) {
697
- throw new Error(`bird ${command} returned unexpected JSON`);
698
- }
699
-
700
- const data = users
701
- .map(toXurlMentionUser)
702
- .filter((user): user is XurlMentionUser => Boolean(user));
703
- const nextToken =
704
- !Array.isArray(rawPayload) && typeof rawPayload.nextCursor === "string"
705
- ? rawPayload.nextCursor
706
- : null;
707
-
708
- return {
709
- data,
710
- meta: {
711
- result_count: data.length,
712
- page_count:
713
- data.length > 0 ? Math.max(1, Math.ceil(data.length / maxResults)) : 1,
714
- next_token: nextToken,
715
- },
716
- };
717
- }
718
-
719
- function normalizeBirdFollowUsersEffect(
720
- payload: unknown,
721
- command: "followers" | "following",
722
- maxResults: number,
723
- ) {
724
- return Effect.try({
725
- try: () => normalizeBirdFollowUsers(payload, command, maxResults),
726
- catch: (error) => error,
727
- });
728
- }
729
-
730
- export function listFollowUsersViaBirdEffect({
731
- direction,
732
- userId,
733
- maxResults,
734
- all,
735
- maxPages,
736
- }: {
737
- direction: "followers" | "following";
738
- userId?: string;
739
- maxResults: number;
740
- all?: boolean;
741
- maxPages?: number;
742
- }): Effect.Effect<XurlFollowUsersResponse, unknown> {
743
- return Effect.gen(function* () {
744
- const args = [direction, "-n", String(maxResults), "--json"];
745
- if (userId) {
746
- args.push("--user", userId);
747
- }
748
- if (all) {
749
- args.push("--all");
750
- }
751
- if (maxPages !== undefined) {
752
- args.push("--max-pages", String(maxPages));
753
- }
754
- const stdout = yield* runBirdJsonCommandEffect(args);
755
- const payload = yield* parseBirdJsonEffect(stdout);
756
- return yield* normalizeBirdFollowUsersEffect(
757
- payload,
758
- direction,
759
- maxResults,
760
- );
761
- });
762
- }
763
-
764
- export function listFollowUsersViaBird(options: {
765
- direction: "followers" | "following";
766
- userId?: string;
767
- maxResults: number;
768
- all?: boolean;
769
- maxPages?: number;
770
- }): Promise<XurlFollowUsersResponse> {
771
- return runEffectPromise(listFollowUsersViaBirdEffect(options));
772
- }
773
-
774
- export function listThreadViaBirdEffect({
775
- tweetId,
776
- all,
777
- maxPages,
778
- timeoutMs,
779
- }: {
780
- tweetId: string;
781
- all?: boolean;
782
- maxPages?: number;
783
- timeoutMs?: number;
784
- }): Effect.Effect<XurlMentionsResponse, unknown> {
785
- return Effect.gen(function* () {
786
- const args = ["thread", tweetId, "--json"];
787
- if (all) {
788
- args.push("--all");
789
- }
790
- if (maxPages !== undefined) {
791
- args.push("--max-pages", String(maxPages));
792
- }
793
- const stdout = yield* runBirdJsonCommandEffect(args, timeoutMs);
794
- const payload = yield* parseBirdJsonEffect(stdout);
795
- return yield* normalizeBirdTweetsPayloadEffect(payload, "thread");
796
- });
797
- }
798
-
799
- export function listThreadViaBird(options: {
800
- tweetId: string;
801
- all?: boolean;
802
- maxPages?: number;
803
- timeoutMs?: number;
804
- }): Promise<XurlMentionsResponse> {
805
- return runEffectPromise(listThreadViaBirdEffect(options));
806
- }
807
-
808
- function normalizeBirdDmsPayloadEffect(payload: unknown) {
809
- return Effect.try({
810
- try: () => {
811
- if (
812
- !payload ||
813
- typeof payload !== "object" ||
814
- (payload as { success?: unknown }).success !== true ||
815
- !Array.isArray(
816
- (payload as { conversations?: unknown }).conversations,
817
- ) ||
818
- !Array.isArray((payload as { events?: unknown }).events)
819
- ) {
820
- throw new Error("bird dms returned unexpected JSON");
821
- }
822
-
823
- return payload as BirdDmsResponse;
824
- },
825
- catch: (error) => error,
826
- });
827
- }
828
-
829
- function parseBirdWhoami(stdout: string): BirdAuthenticatedAccount {
830
- const usernameMatch = stdout.match(/@([A-Za-z0-9_]{1,15})\b/);
831
- if (!usernameMatch?.[1]) {
832
- throw new Error("bird whoami did not report an authenticated username");
833
- }
834
- const id = stdout
835
- .split(/\r?\n/)
836
- .map((line) => line.trim())
837
- .map((line) => {
838
- const labeled = line.match(/(?:🪪|user_?id:?)[^\d]*(\d{2,})/i);
839
- if (labeled?.[1]) {
840
- return labeled[1];
841
- }
842
- if (/[A-Za-z@]/.test(line)) {
843
- return undefined;
844
- }
845
- return line.match(/^\D*(\d{2,})\D*$/)?.[1];
846
- })
847
- .find((value): value is string => Boolean(value));
848
- return {
849
- username: usernameMatch[1],
850
- ...(id ? { id } : {}),
851
- };
852
- }
853
-
854
- export function getAuthenticatedBirdAccountEffect(): Effect.Effect<
855
- BirdAuthenticatedAccount,
856
- unknown
857
- > {
858
- return Effect.gen(function* () {
859
- const stdout = yield* runBirdJsonCommandEffect(["whoami"]);
860
- return yield* Effect.try({
861
- try: () => parseBirdWhoami(stdout),
862
- catch: (error) => error,
863
- });
864
- });
865
- }
866
-
867
- export function getAuthenticatedBirdAccount(): Promise<BirdAuthenticatedAccount> {
868
- return runEffectPromise(getAuthenticatedBirdAccountEffect());
869
- }
870
-
871
- export function listDirectMessagesViaBirdEffect({
872
- maxResults,
873
- inbox = "all",
874
- maxPages,
875
- allPages = false,
876
- pageDelayMs,
877
- }: {
878
- maxResults: number;
879
- inbox?: "all" | "accepted" | "requests";
880
- maxPages?: number;
881
- allPages?: boolean;
882
- pageDelayMs?: number;
883
- }): Effect.Effect<BirdDmsResponse, unknown> {
884
- return Effect.gen(function* () {
885
- const args = ["dms", "-n", String(maxResults), "--json"];
886
- if (inbox !== "all") {
887
- args.push("--inbox", inbox);
888
- }
889
- if (allPages) {
890
- args.push("--all-pages");
891
- } else if (typeof maxPages === "number") {
892
- args.push("--max-pages", String(maxPages));
893
- }
894
- if (typeof pageDelayMs === "number" && pageDelayMs > 0) {
895
- args.push("--page-delay-ms", String(pageDelayMs));
896
- }
897
- const stdout = yield* runBirdJsonCommandEffect(args);
898
- const payload = yield* parseBirdJsonEffect(stdout);
899
- return yield* normalizeBirdDmsPayloadEffect(payload);
900
- });
901
- }
902
-
903
- export function listDirectMessagesViaBird(options: {
904
- maxResults: number;
905
- inbox?: "all" | "accepted" | "requests";
906
- maxPages?: number;
907
- allPages?: boolean;
908
- pageDelayMs?: number;
909
- }): Promise<BirdDmsResponse> {
910
- return runEffectPromise(listDirectMessagesViaBirdEffect(options));
911
- }
912
-
913
- export function runDirectMessageRequestMutationViaBirdEffect({
914
- action,
915
- conversationId,
916
- maxPages,
917
- allPages = false,
918
- }: {
919
- action: BirdDmRequestAction;
920
- conversationId: string;
921
- maxPages?: number;
922
- allPages?: boolean;
923
- }): Effect.Effect<BirdDmMutationResponse, unknown> {
924
- return Effect.gen(function* () {
925
- const command =
926
- action === "accept"
927
- ? "dm-accept"
928
- : action === "reject"
929
- ? "dm-reject"
930
- : "dm-block";
931
- const args = [command, conversationId, "--json"];
932
- if (action === "block") {
933
- if (allPages) {
934
- args.push("--all-pages");
935
- } else if (typeof maxPages === "number") {
936
- args.push("--max-pages", String(maxPages));
937
- }
938
- }
939
- const stdout = yield* runBirdJsonCommandAllowFailureEffect(args);
940
- const payload = yield* parseBirdJsonEffect(stdout);
941
- if (
942
- payload &&
943
- typeof payload === "object" &&
944
- typeof (payload as { success?: unknown }).success === "boolean"
945
- ) {
946
- return payload as BirdDmMutationResponse;
947
- }
948
- throw new Error(`bird ${command} returned unexpected JSON`);
949
- });
950
- }
951
-
952
- export function runDirectMessageRequestMutationViaBird(options: {
953
- action: BirdDmRequestAction;
954
- conversationId: string;
955
- maxPages?: number;
956
- allPages?: boolean;
957
- }): Promise<BirdDmMutationResponse> {
958
- return runEffectPromise(
959
- runDirectMessageRequestMutationViaBirdEffect(options),
960
- );
961
- }
962
-
963
- export function lookupProfileViaBirdEffect(
964
- usernameOrId: string,
965
- ): Effect.Effect<XurlMentionUser | null, unknown> {
966
- return Effect.gen(function* () {
967
- const target = usernameOrId.trim().replace(/^@/, "");
968
- if (!target) {
969
- return null;
970
- }
971
-
972
- const stdout = yield* runBirdJsonCommandEffect([
973
- "user",
974
- target,
975
- "--json",
976
- "--profile-only",
977
- ]).pipe(
978
- Effect.catchAll((error) => {
979
- if (!isUnsupportedBirdOptionError(error, "--profile-only")) {
980
- return Effect.fail(error);
981
- }
982
- return runBirdJsonCommandEffect([
983
- "user",
984
- target,
985
- "--json",
986
- "--count",
987
- "1",
988
- ]);
989
- }),
990
- );
991
- const payload = (yield* parseBirdJsonEffect(
992
- stdout,
993
- )) as BirdUserOverviewPayload;
994
- return toXurlMentionUser(payload.user);
995
- });
996
- }
997
-
998
- export function lookupProfileViaBird(
999
- usernameOrId: string,
1000
- ): Promise<XurlMentionUser | null> {
1001
- return runEffectPromise(lookupProfileViaBirdEffect(usernameOrId));
1002
- }
1003
-
1004
- function toXurlMentionUser(
1005
- user: BirdUserOverviewPayload["user"],
1006
- ): XurlMentionUser | null {
1007
- if (!user?.id || !user.username) {
1008
- return null;
1009
- }
1010
-
1011
- return {
1012
- id: String(user.id),
1013
- username: String(user.username).replace(/^@/, ""),
1014
- name: String(user.name ?? user.username),
1015
- description:
1016
- typeof user.description === "string" ? user.description : undefined,
1017
- location: typeof user.location === "string" ? user.location : undefined,
1018
- url: typeof user.url === "string" ? user.url : undefined,
1019
- verified: typeof user.verified === "boolean" ? user.verified : undefined,
1020
- verified_type:
1021
- typeof user.verifiedType === "string"
1022
- ? user.verifiedType
1023
- : typeof user.verified_type === "string"
1024
- ? user.verified_type
1025
- : undefined,
1026
- profile_image_url:
1027
- typeof user.profileImageUrl === "string"
1028
- ? user.profileImageUrl
1029
- : undefined,
1030
- entities:
1031
- user.entities && typeof user.entities === "object"
1032
- ? user.entities
1033
- : undefined,
1034
- affiliation:
1035
- user.affiliation && typeof user.affiliation === "object"
1036
- ? user.affiliation
1037
- : undefined,
1038
- created_at: typeof user.createdAt === "string" ? user.createdAt : undefined,
1039
- public_metrics: {
1040
- followers_count: Number(user.followersCount ?? 0),
1041
- following_count: Number(user.followingCount ?? 0),
1042
- },
1043
- };
1044
- }
1045
-
1046
- export function lookupProfilesViaBirdEffect(
1047
- usernameOrIds: string[],
1048
- ): Effect.Effect<
1049
- Array<{ target: string; user: XurlMentionUser | null; error?: string }>,
1050
- unknown
1051
- > {
1052
- const targets = Array.from(
1053
- new Set(
1054
- usernameOrIds
1055
- .map((target) => target.trim().replace(/^@/, ""))
1056
- .filter((target) => target.length > 0),
1057
- ),
1058
- );
1059
- if (targets.length === 0) {
1060
- return Effect.succeed([]);
1061
- }
1062
-
1063
- return runBirdJsonCommandEffect(["profiles", ...targets, "--json"]).pipe(
1064
- Effect.flatMap((stdout) =>
1065
- Effect.gen(function* () {
1066
- const payload = (yield* parseBirdJsonEffect(
1067
- stdout,
1068
- )) as BirdProfilesPayload;
1069
- const users = (payload.users ?? [])
1070
- .map(toXurlMentionUser)
1071
- .filter((user): user is XurlMentionUser => Boolean(user));
1072
- const byTarget = new Map<string, XurlMentionUser>();
1073
- for (const user of users) {
1074
- byTarget.set(String(user.id), user);
1075
- byTarget.set(user.username.toLowerCase(), user);
1076
- }
1077
- const errors = new Map(
1078
- (payload.errors ?? []).map((item) => [
1079
- String(item.target ?? "")
1080
- .replace(/^@/, "")
1081
- .toLowerCase(),
1082
- item.error ?? "Unknown error",
1083
- ]),
1084
- );
1085
- return targets.map((target) => ({
1086
- target,
1087
- user: byTarget.get(target.toLowerCase()) ?? null,
1088
- ...(errors.has(target.toLowerCase())
1089
- ? { error: errors.get(target.toLowerCase()) }
1090
- : {}),
1091
- }));
1092
- }),
1093
- ),
1094
- Effect.catchAll((error) => {
1095
- if (!isUnsupportedBirdOptionError(error, "profiles")) {
1096
- return Effect.fail(error);
1097
- }
1098
- return Effect.forEach(
1099
- targets,
1100
- (target) =>
1101
- lookupProfileViaBirdEffect(target).pipe(
1102
- Effect.map((user) => ({ target, user })),
1103
- Effect.catchAll((lookupError) =>
1104
- Effect.succeed({
1105
- target,
1106
- user: null,
1107
- error:
1108
- lookupError instanceof Error
1109
- ? lookupError.message
1110
- : String(lookupError),
1111
- }),
1112
- ),
1113
- ),
1114
- { concurrency: "unbounded" },
1115
- );
1116
- }),
1117
- );
1118
- }
1119
-
1120
- export function lookupProfilesViaBird(
1121
- usernameOrIds: string[],
1122
- ): Promise<
1123
- Array<{ target: string; user: XurlMentionUser | null; error?: string }>
1124
- > {
1125
- return runEffectPromise(lookupProfilesViaBirdEffect(usernameOrIds));
1126
- }
1127
-
1128
- export const __test__ = {
1129
- toIsoTimestamp,
1130
- escapeJsonStringControlChars,
1131
- parseBirdJson,
1132
- formatBirdCommandError,
1133
- isUnsupportedBirdOptionError,
1134
- getBirdTweetItems,
1135
- getBirdTweetItem,
1136
- toMediaEntities,
1137
- toTweetEntities,
1138
- toReferencedTweets,
1139
- normalizeBirdTweets,
1140
- };