birdclaw 0.5.1 → 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.
Files changed (89) hide show
  1. package/CHANGELOG.md +44 -1
  2. package/README.md +50 -5
  3. package/package.json +3 -2
  4. package/scripts/browser-perf.mjs +1 -0
  5. package/scripts/start-test-server.mjs +16 -3
  6. package/src/cli.ts +376 -13
  7. package/src/components/AccountSwitcher.tsx +186 -0
  8. package/src/components/AppNav.tsx +27 -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 +452 -0
  13. package/src/components/SyncNowButton.tsx +57 -25
  14. package/src/components/ThemeSlider.tsx +55 -50
  15. package/src/components/TimelineCard.tsx +225 -93
  16. package/src/components/TimelineRouteFrame.tsx +22 -8
  17. package/src/components/TweetMediaGrid.tsx +87 -38
  18. package/src/components/TweetRichText.tsx +15 -11
  19. package/src/components/account-selection.ts +64 -0
  20. package/src/components/useTimelineRouteData.ts +23 -7
  21. package/src/lib/account-sync-job.ts +654 -0
  22. package/src/lib/actions-transport.ts +216 -146
  23. package/src/lib/api-client.ts +128 -53
  24. package/src/lib/archive-finder.ts +78 -63
  25. package/src/lib/archive-import.ts +1364 -1300
  26. package/src/lib/authored-live.ts +261 -204
  27. package/src/lib/avatar-cache.ts +159 -44
  28. package/src/lib/backup.ts +1532 -951
  29. package/src/lib/bird-actions.ts +139 -57
  30. package/src/lib/bird-command.ts +101 -28
  31. package/src/lib/bird.ts +549 -194
  32. package/src/lib/blocklist.ts +40 -23
  33. package/src/lib/blocks-write.ts +129 -80
  34. package/src/lib/blocks.ts +165 -97
  35. package/src/lib/bookmark-sync-job.ts +250 -160
  36. package/src/lib/conversation-surface.ts +79 -48
  37. package/src/lib/db.ts +33 -3
  38. package/src/lib/dms-live.ts +720 -66
  39. package/src/lib/effect-runtime.ts +45 -0
  40. package/src/lib/follow-graph.ts +224 -180
  41. package/src/lib/http-effect.ts +222 -0
  42. package/src/lib/inbox.ts +74 -43
  43. package/src/lib/link-index.ts +88 -76
  44. package/src/lib/link-insights.ts +24 -0
  45. package/src/lib/link-preview-metadata.ts +472 -52
  46. package/src/lib/media-fetch.ts +286 -213
  47. package/src/lib/mention-threads-live.ts +352 -288
  48. package/src/lib/mentions-live.ts +390 -342
  49. package/src/lib/moderation-target.ts +102 -65
  50. package/src/lib/moderation-write.ts +77 -18
  51. package/src/lib/mutes-write.ts +129 -80
  52. package/src/lib/mutes.ts +8 -1
  53. package/src/lib/openai.ts +84 -53
  54. package/src/lib/period-digest.ts +953 -0
  55. package/src/lib/profile-affiliation-hydration.ts +93 -54
  56. package/src/lib/profile-hydration.ts +124 -72
  57. package/src/lib/profile-replies.ts +60 -43
  58. package/src/lib/profile-resolver.ts +402 -294
  59. package/src/lib/queries.ts +969 -199
  60. package/src/lib/research.ts +165 -120
  61. package/src/lib/sqlite.ts +1 -0
  62. package/src/lib/timeline-collections-live.ts +204 -167
  63. package/src/lib/timeline-live.ts +60 -39
  64. package/src/lib/tweet-lookup.ts +30 -19
  65. package/src/lib/types.ts +38 -1
  66. package/src/lib/ui.ts +30 -7
  67. package/src/lib/url-expansion.ts +226 -55
  68. package/src/lib/url-safety.ts +220 -0
  69. package/src/lib/web-sync.ts +216 -148
  70. package/src/lib/whois.ts +166 -147
  71. package/src/lib/x-web.ts +102 -71
  72. package/src/lib/xurl.ts +681 -411
  73. package/src/routeTree.gen.ts +42 -0
  74. package/src/routes/__root.tsx +25 -5
  75. package/src/routes/api/action.tsx +127 -78
  76. package/src/routes/api/avatar.tsx +39 -30
  77. package/src/routes/api/blocks.tsx +26 -23
  78. package/src/routes/api/conversation.tsx +25 -14
  79. package/src/routes/api/inbox.tsx +27 -21
  80. package/src/routes/api/link-insights.tsx +31 -25
  81. package/src/routes/api/link-preview.tsx +25 -21
  82. package/src/routes/api/period-digest.tsx +123 -0
  83. package/src/routes/api/profile-hydrate.tsx +31 -28
  84. package/src/routes/api/query.tsx +79 -55
  85. package/src/routes/api/status.tsx +18 -10
  86. package/src/routes/api/sync.tsx +75 -29
  87. package/src/routes/dms.tsx +95 -28
  88. package/src/routes/inbox.tsx +32 -19
  89. package/src/routes/today.tsx +441 -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
+ });
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
  }