birdclaw 0.5.0 → 0.6.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.
- package/CHANGELOG.md +63 -0
- package/README.md +55 -5
- package/bin/birdclaw.mjs +50 -11
- package/package.json +9 -7
- package/public/birdclaw-mark.png +0 -0
- package/scripts/browser-perf.mjs +400 -0
- package/scripts/build-docs-site.mjs +940 -0
- package/scripts/docs-site-assets.mjs +311 -0
- package/scripts/run-vitest.mjs +21 -0
- package/scripts/sanitize-node-options.mjs +23 -0
- package/scripts/start-test-server.mjs +42 -0
- package/src/cli.ts +422 -14
- package/src/components/AccountSwitcher.tsx +186 -0
- package/src/components/AppNav.tsx +29 -9
- package/src/components/AvatarChip.tsx +9 -3
- package/src/components/BrandMark.tsx +67 -0
- package/src/components/ConversationThread.tsx +11 -10
- package/src/components/DmWorkspace.tsx +39 -14
- package/src/components/FeedState.tsx +147 -0
- package/src/components/InboxCard.tsx +14 -2
- package/src/components/LinkPreviewCard.tsx +40 -18
- package/src/components/MarkdownViewer.tsx +452 -0
- package/src/components/SavedTimelineView.tsx +64 -56
- package/src/components/SyncNowButton.tsx +137 -0
- package/src/components/ThemeSlider.tsx +49 -93
- package/src/components/TimelineCard.tsx +364 -136
- package/src/components/TimelineRouteFrame.tsx +170 -0
- package/src/components/TweetMediaGrid.tsx +170 -24
- package/src/components/TweetRichText.tsx +28 -13
- package/src/components/account-selection.ts +64 -0
- package/src/components/useTimelineRouteData.ts +153 -0
- package/src/lib/account-sync-job.ts +654 -0
- package/src/lib/actions-transport.ts +216 -146
- package/src/lib/api-client.ts +304 -0
- package/src/lib/archive-finder.ts +72 -53
- package/src/lib/archive-import.ts +1377 -1298
- package/src/lib/authored-live.ts +261 -204
- package/src/lib/avatar-cache.ts +159 -44
- package/src/lib/backup.ts +1532 -951
- package/src/lib/bird-actions.ts +139 -57
- package/src/lib/bird-command.ts +101 -28
- package/src/lib/bird.ts +549 -194
- package/src/lib/blocklist.ts +40 -23
- package/src/lib/blocks-write.ts +129 -80
- package/src/lib/blocks.ts +165 -97
- package/src/lib/bookmark-sync-job.ts +250 -160
- package/src/lib/conversation-surface.ts +205 -0
- package/src/lib/db.ts +35 -3
- package/src/lib/dms-live.ts +720 -66
- package/src/lib/effect-runtime.ts +45 -0
- package/src/lib/follow-graph.ts +224 -180
- package/src/lib/http-effect.ts +222 -0
- package/src/lib/inbox.ts +74 -43
- package/src/lib/link-index.ts +88 -76
- package/src/lib/link-insights.ts +24 -0
- package/src/lib/link-preview-metadata.ts +472 -52
- package/src/lib/media-fetch.ts +286 -213
- package/src/lib/mention-threads-live.ts +352 -288
- package/src/lib/mentions-live.ts +390 -342
- package/src/lib/moderation-target.ts +102 -65
- package/src/lib/moderation-write.ts +77 -18
- package/src/lib/mutes-write.ts +129 -80
- package/src/lib/mutes.ts +8 -1
- package/src/lib/openai.ts +84 -53
- package/src/lib/period-digest.ts +953 -0
- package/src/lib/profile-affiliation-hydration.ts +93 -54
- package/src/lib/profile-hydration.ts +124 -72
- package/src/lib/profile-replies.ts +60 -43
- package/src/lib/profile-resolver.ts +402 -294
- package/src/lib/queries.ts +1024 -189
- package/src/lib/research.ts +165 -120
- package/src/lib/sqlite.ts +1 -0
- package/src/lib/timeline-collections-live.ts +204 -167
- package/src/lib/timeline-live.ts +60 -39
- package/src/lib/tweet-lookup.ts +30 -19
- package/src/lib/types.ts +38 -1
- package/src/lib/ui.ts +41 -10
- package/src/lib/url-expansion.ts +226 -55
- package/src/lib/url-safety.ts +220 -0
- package/src/lib/web-sync.ts +511 -0
- package/src/lib/whois.ts +166 -147
- package/src/lib/x-web.ts +102 -71
- package/src/lib/xurl.ts +681 -411
- package/src/routeTree.gen.ts +63 -0
- package/src/routes/__root.tsx +25 -5
- package/src/routes/api/action.tsx +127 -78
- package/src/routes/api/avatar.tsx +39 -30
- package/src/routes/api/blocks.tsx +26 -23
- package/src/routes/api/conversation.tsx +25 -14
- package/src/routes/api/inbox.tsx +27 -21
- package/src/routes/api/link-insights.tsx +31 -25
- package/src/routes/api/link-preview.tsx +25 -21
- package/src/routes/api/period-digest.tsx +123 -0
- package/src/routes/api/profile-hydrate.tsx +31 -28
- package/src/routes/api/query.tsx +79 -55
- package/src/routes/api/status.tsx +18 -10
- package/src/routes/api/sync.tsx +105 -0
- package/src/routes/dms.tsx +195 -55
- package/src/routes/inbox.tsx +32 -19
- package/src/routes/index.tsx +21 -127
- package/src/routes/links.tsx +50 -5
- package/src/routes/mentions.tsx +21 -127
- package/src/routes/today.tsx +441 -0
- package/src/styles.css +74 -11
- package/vite.config.ts +8 -0
package/src/lib/research.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
tweet.
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
350
|
+
function collectAncestorChainEffect(
|
|
338
351
|
tweetId: string,
|
|
339
352
|
maxThreadDepth: number,
|
|
340
|
-
):
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
visited.
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
|
554
|
+
export function runResearchModeEffect(
|
|
537
555
|
options: ResearchOptions = {},
|
|
538
|
-
):
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
-
|
|
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 };
|