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.
- package/CHANGELOG.md +92 -1
- package/README.md +75 -5
- package/package.json +8 -2
- package/scripts/browser-perf.mjs +1 -0
- package/scripts/start-test-server.mjs +16 -3
- package/src/cli.ts +812 -37
- package/src/components/AccountSwitcher.tsx +186 -0
- package/src/components/AppNav.tsx +37 -7
- package/src/components/AvatarChip.tsx +9 -3
- package/src/components/DmWorkspace.tsx +18 -8
- package/src/components/LinkPreviewCard.tsx +40 -18
- package/src/components/MarkdownViewer.tsx +818 -0
- package/src/components/ProfileAnalysisStream.tsx +428 -0
- package/src/components/ProfilePreview.tsx +120 -9
- package/src/components/SavedTimelineView.tsx +30 -8
- package/src/components/SyncNowButton.tsx +60 -25
- package/src/components/ThemeSlider.tsx +55 -50
- package/src/components/TimelineCard.tsx +240 -92
- package/src/components/TimelineRouteFrame.tsx +38 -8
- package/src/components/TweetMediaGrid.tsx +87 -38
- package/src/components/TweetRichText.tsx +45 -17
- package/src/components/account-selection.ts +64 -0
- package/src/components/useTimelineRouteData.ts +97 -13
- package/src/lib/account-sync-job.ts +666 -0
- package/src/lib/actions-transport.ts +216 -146
- package/src/lib/api-client.ts +128 -53
- package/src/lib/archive-finder.ts +78 -63
- package/src/lib/archive-import.ts +1593 -1291
- package/src/lib/authored-live.ts +262 -204
- package/src/lib/avatar-cache.ts +208 -43
- package/src/lib/backup.ts +1536 -954
- package/src/lib/bird-actions.ts +139 -57
- package/src/lib/bird-command.ts +101 -28
- package/src/lib/bird.ts +582 -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/config.ts +35 -2
- package/src/lib/conversation-surface.ts +79 -48
- package/src/lib/data-sources.ts +219 -0
- package/src/lib/db.ts +95 -4
- 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/geocoding.ts +296 -0
- 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/location.ts +137 -0
- package/src/lib/media-fetch.ts +286 -213
- package/src/lib/mention-threads-live.ts +445 -288
- package/src/lib/mentions-live.ts +549 -354
- 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/network-map.ts +382 -0
- package/src/lib/openai.ts +84 -53
- package/src/lib/period-digest.ts +1317 -0
- package/src/lib/profile-affiliation-hydration.ts +93 -54
- package/src/lib/profile-analysis.ts +1272 -0
- package/src/lib/profile-bio-entities.ts +1 -1
- 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 +983 -203
- package/src/lib/research.ts +165 -120
- package/src/lib/search-discussion.ts +1016 -0
- package/src/lib/sqlite.ts +1 -0
- package/src/lib/timeline-collections-live.ts +204 -167
- package/src/lib/timeline-live.ts +325 -51
- package/src/lib/tweet-account-edges.ts +2 -0
- package/src/lib/tweet-lookup.ts +30 -19
- package/src/lib/tweet-render.ts +141 -1
- package/src/lib/tweet-search-live.ts +565 -0
- package/src/lib/types.ts +75 -3
- package/src/lib/ui.ts +31 -8
- package/src/lib/url-expansion.ts +226 -55
- package/src/lib/url-safety.ts +220 -0
- package/src/lib/web-sync.ts +222 -149
- package/src/lib/whois.ts +166 -147
- package/src/lib/x-web.ts +102 -71
- package/src/lib/xurl-rate-limits.ts +267 -0
- package/src/lib/xurl.ts +1185 -405
- package/src/routeTree.gen.ts +273 -0
- package/src/routes/__root.tsx +24 -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/data-sources.tsx +24 -0
- 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/network-map.tsx +55 -0
- package/src/routes/api/period-digest.tsx +133 -0
- package/src/routes/api/profile-analysis.tsx +152 -0
- package/src/routes/api/profile-hydrate.tsx +31 -28
- package/src/routes/api/query.tsx +80 -55
- package/src/routes/api/search-discussion.tsx +169 -0
- package/src/routes/api/status.tsx +18 -10
- package/src/routes/api/sync.tsx +75 -29
- package/src/routes/api/xurl-rate-limits.tsx +24 -0
- package/src/routes/data-sources.tsx +255 -0
- package/src/routes/discuss.tsx +419 -0
- package/src/routes/dms.tsx +95 -28
- package/src/routes/inbox.tsx +32 -19
- package/src/routes/network-map.tsx +1035 -0
- package/src/routes/profile-analyze.tsx +112 -0
- package/src/routes/profiles.$handle.tsx +228 -0
- package/src/routes/rate-limits.tsx +309 -0
- package/src/routes/today.tsx +455 -0
- 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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
}
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
57
|
+
}).pipe(Effect.catchAll(() => Effect.succeed(null)));
|
|
60
58
|
}
|
|
61
59
|
|
|
62
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
const candidates = yield* Effect.forEach(
|
|
72
|
+
matches,
|
|
73
|
+
(entry) => getCandidateEffect(path.join(directoryPath, entry)),
|
|
74
|
+
{ concurrency: "unbounded" },
|
|
75
|
+
);
|
|
77
76
|
|
|
78
|
-
|
|
77
|
+
return candidates.filter((item) => item !== null);
|
|
78
|
+
}).pipe(Effect.catchAll(() => Effect.succeed([])));
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
}
|