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
@@ -0,0 +1,251 @@
1
+ import { Effect } from "effect";
2
+ import { tryPromise } from "./effect-runtime";
3
+ import {
4
+ defaultRuntimeServices,
5
+ type RuntimeServices,
6
+ } from "./runtime-services";
7
+
8
+ const DEFAULT_DELIMITER_PATTERN = /\n---\s*\n/;
9
+ const DEFAULT_DELIMITER_HOLD = 8;
10
+
11
+ export interface OpenAIStreamState {
12
+ eventBuffer: string;
13
+ rawText: string;
14
+ pendingVisible: string;
15
+ jsonMode: boolean;
16
+ responseId?: string;
17
+ usage?: unknown;
18
+ error?: string;
19
+ }
20
+
21
+ export interface OpenAIStreamResult {
22
+ rawText: string;
23
+ responseId?: string;
24
+ usage?: unknown;
25
+ }
26
+
27
+ function toError(error: unknown) {
28
+ return error instanceof Error ? error : new Error(String(error));
29
+ }
30
+
31
+ export function createOpenAIStreamState(): OpenAIStreamState {
32
+ return {
33
+ eventBuffer: "",
34
+ rawText: "",
35
+ pendingVisible: "",
36
+ jsonMode: false,
37
+ };
38
+ }
39
+
40
+ function emitVisibleDelta(
41
+ state: OpenAIStreamState,
42
+ delta: string,
43
+ onDelta: ((delta: string) => void) | undefined,
44
+ delimiterPattern: RegExp,
45
+ delimiterHold: number,
46
+ ) {
47
+ state.rawText += delta;
48
+ if (state.jsonMode) return;
49
+
50
+ const combined = state.pendingVisible + delta;
51
+ const delimiterIndex = combined.search(delimiterPattern);
52
+ if (delimiterIndex >= 0) {
53
+ const visible = combined.slice(0, delimiterIndex);
54
+ if (visible) onDelta?.(visible);
55
+ state.pendingVisible = "";
56
+ state.jsonMode = true;
57
+ return;
58
+ }
59
+
60
+ if (combined.length <= delimiterHold) {
61
+ state.pendingVisible = combined;
62
+ return;
63
+ }
64
+
65
+ const visible = combined.slice(0, -delimiterHold);
66
+ state.pendingVisible = combined.slice(-delimiterHold);
67
+ if (visible) onDelta?.(visible);
68
+ }
69
+
70
+ function handleOpenAIEvent(
71
+ state: OpenAIStreamState,
72
+ event: Record<string, unknown>,
73
+ onDelta: ((delta: string) => void) | undefined,
74
+ delimiterPattern: RegExp,
75
+ delimiterHold: number,
76
+ ) {
77
+ const type = typeof event.type === "string" ? event.type : "";
78
+ if (
79
+ type === "response.output_text.delta" &&
80
+ typeof event.delta === "string"
81
+ ) {
82
+ emitVisibleDelta(
83
+ state,
84
+ event.delta,
85
+ onDelta,
86
+ delimiterPattern,
87
+ delimiterHold,
88
+ );
89
+ return;
90
+ }
91
+ if (type === "response.completed") {
92
+ const response = event.response;
93
+ if (response && typeof response === "object") {
94
+ const record = response as Record<string, unknown>;
95
+ state.responseId = typeof record.id === "string" ? record.id : undefined;
96
+ state.usage = record.usage;
97
+ }
98
+ return;
99
+ }
100
+ if (type === "response.error" || type === "error") {
101
+ const error = event.error;
102
+ state.error =
103
+ error && typeof error === "object" && "message" in error
104
+ ? String((error as { message?: unknown }).message)
105
+ : "OpenAI stream failed";
106
+ return;
107
+ }
108
+ if (type === "response.failed" || type === "response.incomplete") {
109
+ const response = event.response;
110
+ const record =
111
+ response && typeof response === "object"
112
+ ? (response as Record<string, unknown>)
113
+ : {};
114
+ const error = record.error;
115
+ const incomplete = record.incomplete_details;
116
+ state.error =
117
+ error && typeof error === "object" && "message" in error
118
+ ? String((error as { message?: unknown }).message)
119
+ : incomplete && typeof incomplete === "object" && "reason" in incomplete
120
+ ? `OpenAI response incomplete: ${String((incomplete as { reason?: unknown }).reason)}`
121
+ : "OpenAI stream failed";
122
+ }
123
+ }
124
+
125
+ export function processOpenAIResponseSseChunk(
126
+ state: OpenAIStreamState,
127
+ chunk: string,
128
+ {
129
+ onDelta,
130
+ delimiterPattern = DEFAULT_DELIMITER_PATTERN,
131
+ delimiterHold = DEFAULT_DELIMITER_HOLD,
132
+ }: {
133
+ onDelta?: (delta: string) => void;
134
+ delimiterPattern?: RegExp;
135
+ delimiterHold?: number;
136
+ } = {},
137
+ ) {
138
+ state.eventBuffer += chunk;
139
+ let boundary = state.eventBuffer.indexOf("\n\n");
140
+ while (boundary >= 0) {
141
+ const block = state.eventBuffer.slice(0, boundary);
142
+ state.eventBuffer = state.eventBuffer.slice(boundary + 2);
143
+ const data = block
144
+ .split("\n")
145
+ .filter((line) => line.startsWith("data:"))
146
+ .map((line) => line.slice(5).trimStart())
147
+ .join("\n");
148
+ if (data && data !== "[DONE]") {
149
+ try {
150
+ handleOpenAIEvent(
151
+ state,
152
+ JSON.parse(data) as Record<string, unknown>,
153
+ onDelta,
154
+ delimiterPattern,
155
+ delimiterHold,
156
+ );
157
+ } catch {
158
+ // The feature parser decides whether partial output remains usable.
159
+ }
160
+ }
161
+ boundary = state.eventBuffer.indexOf("\n\n");
162
+ }
163
+ }
164
+
165
+ export function readOpenAIResponseStreamEffect(
166
+ response: Response,
167
+ options: {
168
+ onDelta?: (delta: string) => void;
169
+ delimiterPattern?: RegExp;
170
+ delimiterHold?: number;
171
+ } = {},
172
+ ): Effect.Effect<OpenAIStreamResult, Error> {
173
+ const reader = response.body?.getReader();
174
+ if (!reader) {
175
+ return Effect.fail(new Error("OpenAI response did not include a stream"));
176
+ }
177
+ const decoder = new TextDecoder();
178
+
179
+ return Effect.gen(function* () {
180
+ const state = createOpenAIStreamState();
181
+ for (;;) {
182
+ const { done, value } = yield* tryPromise(() => reader.read()).pipe(
183
+ Effect.mapError(toError),
184
+ );
185
+ if (!done) {
186
+ processOpenAIResponseSseChunk(
187
+ state,
188
+ decoder.decode(value, { stream: true }),
189
+ options,
190
+ );
191
+ continue;
192
+ }
193
+ if (!state.jsonMode && state.pendingVisible) {
194
+ options.onDelta?.(state.pendingVisible);
195
+ }
196
+ if (state.error) {
197
+ return yield* Effect.fail(new Error(state.error));
198
+ }
199
+ return {
200
+ rawText: state.rawText,
201
+ ...(state.responseId ? { responseId: state.responseId } : {}),
202
+ ...(state.usage === undefined ? {} : { usage: state.usage }),
203
+ };
204
+ }
205
+ }).pipe(
206
+ Effect.ensuring(
207
+ Effect.sync(() => {
208
+ reader.releaseLock();
209
+ }),
210
+ ),
211
+ );
212
+ }
213
+
214
+ export function requestOpenAIResponseEffect({
215
+ body,
216
+ signal,
217
+ runtime = defaultRuntimeServices,
218
+ }: {
219
+ body: unknown;
220
+ signal?: AbortSignal;
221
+ runtime?: RuntimeServices;
222
+ }): Effect.Effect<Response, Error> {
223
+ return Effect.gen(function* () {
224
+ const apiKey = runtime.env("OPENAI_API_KEY");
225
+ if (!apiKey) {
226
+ return yield* Effect.fail(new Error("OPENAI_API_KEY is not set"));
227
+ }
228
+ const response = yield* tryPromise(() =>
229
+ runtime.fetch("https://api.openai.com/v1/responses", {
230
+ method: "POST",
231
+ signal,
232
+ headers: {
233
+ authorization: `Bearer ${apiKey}`,
234
+ "content-type": "application/json",
235
+ },
236
+ body: JSON.stringify(body),
237
+ }),
238
+ ).pipe(Effect.mapError(toError));
239
+ if (!response.ok) {
240
+ const text = yield* tryPromise(() => response.text()).pipe(
241
+ Effect.mapError(toError),
242
+ );
243
+ return yield* Effect.fail(
244
+ new Error(
245
+ `OpenAI request failed: ${String(response.status)} ${text.slice(0, 400)}`,
246
+ ),
247
+ );
248
+ }
249
+ return response;
250
+ });
251
+ }
@@ -0,0 +1,93 @@
1
+ import { Effect } from "effect";
2
+ import {
3
+ runSyncPlanEffect,
4
+ type PaginationPageContext,
5
+ type PaginationStopReason,
6
+ } from "./sync-plan";
7
+
8
+ export type { PaginationPageContext, PaginationStopReason } from "./sync-plan";
9
+
10
+ export interface PaginatedSyncResult<Page> {
11
+ complete: boolean;
12
+ fetched: number;
13
+ nextCursor?: string;
14
+ pages: Page[];
15
+ stopReason: PaginationStopReason;
16
+ }
17
+
18
+ export function collectPaginatedEffect<Page, ErrorType>({
19
+ fetchPage,
20
+ getItemCount,
21
+ getNextCursor,
22
+ initialCursor,
23
+ maxItems,
24
+ maxPages,
25
+ onPage,
26
+ pageDelayMs,
27
+ shouldStop,
28
+ }: {
29
+ fetchPage: (context: {
30
+ cursor?: string;
31
+ fetched: number;
32
+ pageIndex: number;
33
+ }) => Effect.Effect<Page, ErrorType>;
34
+ getItemCount?: (page: Page) => number;
35
+ getNextCursor: (page: Page) => string | null | undefined;
36
+ initialCursor?: string;
37
+ maxItems?: number;
38
+ maxPages?: number;
39
+ onPage?: (context: PaginationPageContext<Page>) => void;
40
+ pageDelayMs?: number;
41
+ shouldStop?: (context: Omit<PaginationPageContext<Page>, "done">) => boolean;
42
+ }): Effect.Effect<PaginatedSyncResult<Page>, ErrorType> {
43
+ return runSyncPlanEffect({
44
+ fetchPage,
45
+ getNextCursor,
46
+ ...(getItemCount ? { getItemCount } : {}),
47
+ ...(initialCursor ? { initialCursor } : {}),
48
+ ...(maxItems !== undefined ? { maxItems } : {}),
49
+ ...(maxPages !== undefined ? { maxPages } : {}),
50
+ ...(onPage
51
+ ? {
52
+ onPage: (context: {
53
+ cursor?: string;
54
+ fetched: number;
55
+ page: Page;
56
+ pageIndex: number;
57
+ pageNumber: number;
58
+ stopReason?: PaginationStopReason | "error";
59
+ done: boolean;
60
+ }) =>
61
+ onPage({
62
+ ...context,
63
+ stopReason: context.stopReason as
64
+ | PaginationStopReason
65
+ | undefined,
66
+ }),
67
+ }
68
+ : {}),
69
+ ...(pageDelayMs !== undefined ? { pageDelayMs } : {}),
70
+ ...(shouldStop
71
+ ? {
72
+ shouldStop: (context: {
73
+ cursor?: string;
74
+ fetched: number;
75
+ page: Page;
76
+ pageIndex: number;
77
+ pageNumber: number;
78
+ }) => shouldStop(context),
79
+ }
80
+ : {}),
81
+ }).pipe(
82
+ Effect.map(
83
+ (result) =>
84
+ ({
85
+ complete: result.complete,
86
+ fetched: result.fetched,
87
+ ...(result.nextCursor ? { nextCursor: result.nextCursor } : {}),
88
+ pages: result.pages,
89
+ stopReason: result.stopReason as PaginationStopReason,
90
+ }) satisfies PaginatedSyncResult<Page>,
91
+ ),
92
+ );
93
+ }