birdclaw 0.8.1 → 0.8.3

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 (103) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/package.json +2 -4
  3. package/scripts/browser-perf.mjs +27 -0
  4. package/src/cli/command-context.ts +17 -0
  5. package/src/cli/register-analysis.ts +500 -0
  6. package/src/cli/register-compose.ts +40 -0
  7. package/src/cli/register-graph.ts +132 -0
  8. package/src/cli/register-inbox.ts +41 -0
  9. package/src/cli/register-storage.ts +106 -0
  10. package/src/cli.ts +30 -750
  11. package/src/components/AccountSwitcher.tsx +7 -15
  12. package/src/components/AvatarChip.tsx +1 -1
  13. package/src/components/AvatarPreload.ts +149 -0
  14. package/src/components/ConversationThread.tsx +4 -0
  15. package/src/components/EmbeddedTweetCard.tsx +4 -0
  16. package/src/components/FloatingPreview.tsx +382 -0
  17. package/src/components/MarkdownCitations.tsx +680 -0
  18. package/src/components/MarkdownViewer.tsx +7 -715
  19. package/src/components/ProfileAnalysisClient.ts +191 -0
  20. package/src/components/ProfileAnalysisStream.tsx +16 -185
  21. package/src/components/ProfilePreview.tsx +41 -81
  22. package/src/components/TimelineCard.tsx +18 -2
  23. package/src/components/TweetArticleCard.tsx +66 -0
  24. package/src/components/TweetRichText.tsx +33 -2
  25. package/src/components/links-controller.ts +162 -0
  26. package/src/components/links-model.ts +198 -0
  27. package/src/components/network-map-controller.ts +84 -0
  28. package/src/components/network-map-model.ts +255 -0
  29. package/src/components/useDebouncedValue.ts +12 -0
  30. package/src/components/useTimelineRouteData.ts +142 -170
  31. package/src/lib/analysis-runtime.ts +238 -0
  32. package/src/lib/api-client.ts +16 -100
  33. package/src/lib/api-contracts.ts +328 -0
  34. package/src/lib/archive-finder.ts +38 -0
  35. package/src/lib/archive-import-plan.ts +102 -0
  36. package/src/lib/archive-import.ts +170 -239
  37. package/src/lib/authored-live.ts +77 -182
  38. package/src/lib/backup.ts +335 -424
  39. package/src/lib/bird.ts +32 -1
  40. package/src/lib/blocks-write.ts +30 -26
  41. package/src/lib/blocks.ts +18 -20
  42. package/src/lib/database-metrics.ts +88 -0
  43. package/src/lib/database-migrations.ts +34 -0
  44. package/src/lib/database-schema.ts +312 -0
  45. package/src/lib/database-writer.ts +69 -0
  46. package/src/lib/db.ts +141 -334
  47. package/src/lib/dm-read-model.ts +533 -0
  48. package/src/lib/dms-live.ts +34 -97
  49. package/src/lib/follow-graph.ts +17 -27
  50. package/src/lib/import-repository.ts +138 -0
  51. package/src/lib/inbox.ts +2 -1
  52. package/src/lib/live-sync-engine.ts +209 -0
  53. package/src/lib/live-transport-gateway.ts +128 -0
  54. package/src/lib/mention-threads-live.ts +90 -176
  55. package/src/lib/mentions-export.ts +1 -1
  56. package/src/lib/mentions-live.ts +57 -225
  57. package/src/lib/moderation-target.ts +15 -4
  58. package/src/lib/moderation-write.ts +1 -1
  59. package/src/lib/mutes-write.ts +30 -26
  60. package/src/lib/openai-response-runtime.ts +251 -0
  61. package/src/lib/paginated-sync.ts +93 -0
  62. package/src/lib/period-digest.ts +116 -304
  63. package/src/lib/profile-analysis.ts +37 -111
  64. package/src/lib/queries.ts +6 -2380
  65. package/src/lib/query-actions.ts +437 -0
  66. package/src/lib/query-client.tsx +47 -0
  67. package/src/lib/query-read-model-shared.ts +52 -0
  68. package/src/lib/query-read-models.ts +5 -0
  69. package/src/lib/query-resource.ts +41 -0
  70. package/src/lib/query-status.ts +164 -0
  71. package/src/lib/research.ts +1 -1
  72. package/src/lib/runtime-services.ts +20 -0
  73. package/src/lib/search-discussion.ts +75 -279
  74. package/src/lib/server-runtime-services.ts +30 -0
  75. package/src/lib/sqlite.ts +53 -14
  76. package/src/lib/streaming-ingestion.ts +240 -0
  77. package/src/lib/sync-cache.ts +6 -1
  78. package/src/lib/sync-plan.ts +175 -0
  79. package/src/lib/timeline-collections-live.ts +83 -256
  80. package/src/lib/timeline-live.ts +86 -235
  81. package/src/lib/timeline-read-model.ts +1191 -0
  82. package/src/lib/tweet-render.ts +78 -0
  83. package/src/lib/tweet-repository.ts +156 -0
  84. package/src/lib/tweet-search-live.ts +63 -166
  85. package/src/lib/types.ts +8 -0
  86. package/src/lib/ui.ts +1 -1
  87. package/src/lib/web-sync.ts +67 -50
  88. package/src/lib/whois.ts +2 -1
  89. package/src/router.tsx +1 -1
  90. package/src/routes/__root.tsx +11 -21
  91. package/src/routes/api/action.tsx +1 -1
  92. package/src/routes/api/conversation.tsx +1 -1
  93. package/src/routes/api/query.tsx +32 -26
  94. package/src/routes/api/status.tsx +6 -2
  95. package/src/routes/api/sync.tsx +5 -2
  96. package/src/routes/blocks.tsx +97 -131
  97. package/src/routes/data-sources.tsx +17 -25
  98. package/src/routes/dms.tsx +168 -167
  99. package/src/routes/inbox.tsx +63 -57
  100. package/src/routes/links.tsx +31 -329
  101. package/src/routes/network-map.tsx +41 -344
  102. package/src/routes/rate-limits.tsx +17 -21
  103. package/vite.config.ts +0 -2
@@ -1,13 +1,16 @@
1
1
  import { createFileRoute } from "@tanstack/react-router";
2
+ import {
3
+ keepPreviousData,
4
+ useQuery,
5
+ useQueryClient,
6
+ } from "@tanstack/react-query";
2
7
  import { useEffect, useMemo, useState } from "react";
3
8
  import { AvatarChip } from "#/components/AvatarChip";
9
+ import { useDebouncedValue } from "#/components/useDebouncedValue";
10
+ import { fetchQueryEnvelope, postAction } from "#/lib/api-client";
4
11
  import { formatCompactNumber } from "#/lib/present";
5
- import type {
6
- BlockItem,
7
- BlockListResponse,
8
- BlockSearchItem,
9
- QueryEnvelope,
10
- } from "#/lib/types";
12
+ import { queryKeys } from "#/lib/query-client";
13
+ import type { BlockListResponse } from "#/lib/types";
11
14
  import {
12
15
  blockRowBodyClass,
13
16
  blockRowClass,
@@ -35,128 +38,96 @@ export const Route = createFileRoute("/blocks")({
35
38
  });
36
39
 
37
40
  function BlocksRoute() {
38
- const [meta, setMeta] = useState<QueryEnvelope | null>(null);
41
+ const queryClient = useQueryClient();
39
42
  const [accountId, setAccountId] = useState<string>("acct_primary");
40
43
  const [search, setSearch] = useState("");
41
- const [items, setItems] = useState<BlockItem[]>([]);
42
- const [matches, setMatches] = useState<BlockSearchItem[]>([]);
43
- const [refreshTick, setRefreshTick] = useState(0);
44
44
  const [isSubmitting, setIsSubmitting] = useState(false);
45
- const [isSyncing, setIsSyncing] = useState(false);
46
45
  const [message, setMessage] = useState("");
47
- const [error, setError] = useState("");
46
+ const [actionError, setActionError] = useState("");
47
+ const statusQuery = useQuery({
48
+ queryKey: queryKeys.status,
49
+ queryFn: ({ signal }) => fetchQueryEnvelope({ signal }),
50
+ });
51
+ const meta = statusQuery.data ?? null;
52
+ const debouncedSearch = useDebouncedValue(search, 180);
48
53
  const hasAccountId = accountId.trim().length > 0;
49
54
  const isReady = Boolean(meta);
50
-
51
- useEffect(() => {
52
- const controller = new AbortController();
53
-
54
- fetch("/api/status", { signal: controller.signal })
55
- .then((response) => response.json())
56
- .then((data: QueryEnvelope) => {
57
- setMeta(data);
58
- setAccountId(data.accounts[0]?.id ?? "acct_primary");
59
- setError("");
60
- })
61
- .catch((error: unknown) => {
62
- if (error instanceof DOMException && error.name === "AbortError") {
63
- return;
64
- }
65
- setError(
66
- error instanceof Error
67
- ? error.message
68
- : "Unable to load blocklist status",
69
- );
55
+ const blocksQueryKey = [
56
+ ...queryKeys.blocks,
57
+ { accountId, search: debouncedSearch },
58
+ ] as const;
59
+ const blocksQuery = useQuery({
60
+ queryKey: blocksQueryKey,
61
+ enabled: hasAccountId,
62
+ queryFn: async ({ signal }) => {
63
+ const params = new URLSearchParams({
64
+ account: accountId,
65
+ limit: "12",
70
66
  });
71
-
72
- return () => {
73
- controller.abort();
74
- };
75
- }, []);
76
-
77
- useEffect(() => {
78
- const controller = new AbortController();
79
- const params = new URLSearchParams({
80
- account: accountId,
81
- limit: "12",
82
- refresh: String(refreshTick),
83
- });
84
- if (search.trim()) {
85
- params.set("search", search.trim());
86
- }
87
-
88
- fetch(`/api/blocks?${params.toString()}`, { signal: controller.signal })
89
- .then((response) => response.json())
90
- .then((data: BlockListResponse) => {
91
- setItems(data.items);
92
- setMatches(data.matches);
93
- setError("");
94
- })
95
- .catch((error: unknown) => {
96
- if (error instanceof DOMException && error.name === "AbortError") {
97
- return;
98
- }
99
- setError(
100
- error instanceof Error ? error.message : "Unable to load blocklist",
101
- );
67
+ if (debouncedSearch.trim()) {
68
+ params.set("search", debouncedSearch.trim());
69
+ }
70
+ const response = await fetch(`/api/blocks?${params.toString()}`, {
71
+ signal,
102
72
  });
103
-
104
- return () => {
105
- controller.abort();
106
- };
107
- }, [accountId, refreshTick, search]);
108
-
109
- useEffect(() => {
110
- if (!hasAccountId) {
111
- return;
112
- }
113
-
114
- const controller = new AbortController();
115
- setIsSyncing(true);
116
-
117
- fetch("/api/action", {
118
- method: "POST",
119
- headers: { "content-type": "application/json" },
120
- body: JSON.stringify({
73
+ if (!response.ok) {
74
+ throw new Error(
75
+ `Blocklist request failed (${String(response.status)})`,
76
+ );
77
+ }
78
+ return (await response.json()) as BlockListResponse;
79
+ },
80
+ placeholderData: keepPreviousData,
81
+ staleTime: 5 * 60_000,
82
+ });
83
+ const items = blocksQuery.data?.items ?? [];
84
+ const matches = blocksQuery.data?.matches ?? [];
85
+ const blockSyncQuery = useQuery({
86
+ queryKey: [...queryKeys.blockSync, accountId],
87
+ enabled: hasAccountId,
88
+ retry: false,
89
+ queryFn: async () => {
90
+ const data = (await postAction({
121
91
  kind: "syncBlocks",
122
92
  accountId,
123
- }),
124
- signal: controller.signal,
125
- })
126
- .then((response) => response.json())
127
- .then(
128
- (data: {
129
- ok?: boolean;
130
- synced?: boolean;
131
- syncedCount?: number;
132
- transport?: { ok?: boolean; output?: string };
133
- }) => {
134
- if (data.ok === false) {
135
- setError(data.transport?.output ?? "Block sync failed");
136
- return;
137
- }
138
- setRefreshTick((value) => value + 1);
139
- if (data.transport?.output?.includes("disabled")) {
140
- return;
141
- }
142
- setMessage(
143
- data.transport?.output ??
144
- `Synced ${String(data.syncedCount ?? 0)} remote blocks`,
145
- );
146
- },
147
- )
148
- .catch((error: unknown) => {
149
- if (error instanceof DOMException && error.name === "AbortError") {
150
- return;
151
- }
152
- setError(error instanceof Error ? error.message : "Block sync failed");
153
- })
154
- .finally(() => setIsSyncing(false));
93
+ })) as {
94
+ ok?: boolean;
95
+ syncedCount?: number;
96
+ transport?: { ok?: boolean; output?: string };
97
+ };
98
+ if (data.ok === false || data.transport?.ok === false) {
99
+ throw new Error(data.transport?.output ?? "Block sync failed");
100
+ }
101
+ await queryClient.invalidateQueries({ queryKey: queryKeys.blocks });
102
+ return data;
103
+ },
104
+ staleTime: 5 * 60_000,
105
+ });
106
+ const isSyncing = blockSyncQuery.isFetching;
107
+ const queryError =
108
+ statusQuery.error ?? blocksQuery.error ?? blockSyncQuery.error ?? null;
109
+ const error =
110
+ actionError ||
111
+ (queryError instanceof Error
112
+ ? queryError.message
113
+ : queryError
114
+ ? "Unable to load blocklist"
115
+ : "");
155
116
 
156
- return () => {
157
- controller.abort();
158
- };
159
- }, [accountId, hasAccountId]);
117
+ useEffect(() => {
118
+ if (!meta?.accounts.length) return;
119
+ if (meta.accounts.some((account) => account.id === accountId)) return;
120
+ setAccountId(meta.accounts[0]?.id ?? "acct_primary");
121
+ }, [accountId, meta]);
122
+
123
+ useEffect(() => {
124
+ const data = blockSyncQuery.data;
125
+ if (!data || data.transport?.output?.includes("disabled")) return;
126
+ setMessage(
127
+ data.transport?.output ??
128
+ `Synced ${String(data.syncedCount ?? 0)} remote blocks`,
129
+ );
130
+ }, [blockSyncQuery.data]);
160
131
 
161
132
  const subtitle = useMemo(() => {
162
133
  if (!meta) {
@@ -177,26 +148,21 @@ function BlocksRoute() {
177
148
  if (!normalized) return;
178
149
 
179
150
  setIsSubmitting(true);
180
- setError("");
151
+ setActionError("");
181
152
  setMessage("");
182
153
 
183
154
  try {
184
- const response = await fetch("/api/action", {
185
- method: "POST",
186
- headers: { "content-type": "application/json" },
187
- body: JSON.stringify({
188
- kind,
189
- accountId,
190
- query: normalized,
191
- }),
192
- });
193
- const data = (await response.json()) as {
155
+ const data = (await postAction({
156
+ kind,
157
+ accountId,
158
+ query: normalized,
159
+ })) as {
194
160
  ok?: boolean;
195
161
  profile?: { handle?: string };
196
162
  transport?: { ok?: boolean; output?: string };
197
163
  };
198
164
  if (data.ok === false || data.transport?.ok === false) {
199
- setError(data.transport?.output ?? "Blocklist action failed");
165
+ setActionError(data.transport?.output ?? "Blocklist action failed");
200
166
  return;
201
167
  }
202
168
 
@@ -205,9 +171,9 @@ function BlocksRoute() {
205
171
  data.profile?.handle ?? normalized.replace(/^@/, "")
206
172
  } · ${data.transport?.output ?? "local"}`,
207
173
  );
208
- setRefreshTick((value) => value + 1);
174
+ await queryClient.invalidateQueries({ queryKey: queryKeys.blocks });
209
175
  } catch (submitError) {
210
- setError(
176
+ setActionError(
211
177
  submitError instanceof Error
212
178
  ? submitError.message
213
179
  : "Blocklist action failed",
@@ -1,4 +1,5 @@
1
1
  import { createFileRoute } from "@tanstack/react-router";
2
+ import { useQuery } from "@tanstack/react-query";
2
3
  import {
3
4
  AlertTriangle,
4
5
  CheckCircle2,
@@ -8,7 +9,8 @@ import {
8
9
  TerminalSquare,
9
10
  XCircle,
10
11
  } from "lucide-react";
11
- import { useCallback, useEffect, useMemo, useState } from "react";
12
+ import { useMemo } from "react";
13
+ import { queryKeys } from "#/lib/query-client";
12
14
  import type {
13
15
  LiveDataSourceCapability,
14
16
  LiveDataSourceKind,
@@ -169,11 +171,14 @@ function CapabilityRow({
169
171
  }
170
172
 
171
173
  function DataSourcesRoute() {
172
- const [snapshot, setSnapshot] = useState<LiveDataSourcesResponse | null>(
173
- null,
174
- );
175
- const [error, setError] = useState<string | null>(null);
176
- const [loading, setLoading] = useState(true);
174
+ const dataSourcesQuery = useQuery({
175
+ queryKey: queryKeys.dataSources,
176
+ queryFn: fetchDataSources,
177
+ staleTime: 5 * 60_000,
178
+ });
179
+ const snapshot = dataSourcesQuery.data ?? null;
180
+ const loading = dataSourcesQuery.isFetching;
181
+ const error = dataSourcesQuery.error;
177
182
  const sourcesByKind = useMemo(
178
183
  () =>
179
184
  new Map(
@@ -182,23 +187,6 @@ function DataSourcesRoute() {
182
187
  [snapshot],
183
188
  );
184
189
 
185
- const load = useCallback(() => {
186
- setLoading(true);
187
- setError(null);
188
- fetchDataSources()
189
- .then(setSnapshot)
190
- .catch((cause: unknown) => {
191
- setError(
192
- cause instanceof Error ? cause.message : "Data sources unavailable",
193
- );
194
- })
195
- .finally(() => setLoading(false));
196
- }, []);
197
-
198
- useEffect(() => {
199
- load();
200
- }, [load]);
201
-
202
190
  return (
203
191
  <section className="flex min-h-screen flex-col">
204
192
  <header className={pageHeaderClass}>
@@ -213,7 +201,7 @@ function DataSourcesRoute() {
213
201
  <button
214
202
  className={secondaryButtonClass}
215
203
  type="button"
216
- onClick={load}
204
+ onClick={() => void dataSourcesQuery.refetch()}
217
205
  disabled={loading}
218
206
  >
219
207
  <RefreshCw className={cx("size-4", loading && "animate-spin")} />
@@ -222,7 +210,11 @@ function DataSourcesRoute() {
222
210
  </div>
223
211
  </div>
224
212
  </header>
225
- {error ? <div className={errorCopyClass}>{error}</div> : null}
213
+ {error ? (
214
+ <div className={errorCopyClass}>
215
+ {error instanceof Error ? error.message : "Data sources unavailable"}
216
+ </div>
217
+ ) : null}
226
218
  {snapshot ? (
227
219
  <>
228
220
  <div className="grid border-t border-[var(--line)]">