birdclaw 0.8.2 → 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 (89) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +2 -1
  3. package/src/cli/command-context.ts +17 -0
  4. package/src/cli/register-analysis.ts +500 -0
  5. package/src/cli/register-compose.ts +40 -0
  6. package/src/cli/register-graph.ts +132 -0
  7. package/src/cli/register-inbox.ts +41 -0
  8. package/src/cli/register-storage.ts +106 -0
  9. package/src/cli.ts +30 -750
  10. package/src/components/AccountSwitcher.tsx +7 -15
  11. package/src/components/AvatarChip.tsx +1 -1
  12. package/src/components/AvatarPreload.ts +149 -0
  13. package/src/components/MarkdownCitations.tsx +680 -0
  14. package/src/components/MarkdownViewer.tsx +8 -674
  15. package/src/components/ProfileAnalysisClient.ts +191 -0
  16. package/src/components/ProfileAnalysisStream.tsx +16 -185
  17. package/src/components/ProfilePreview.tsx +2 -0
  18. package/src/components/links-controller.ts +162 -0
  19. package/src/components/links-model.ts +198 -0
  20. package/src/components/network-map-controller.ts +84 -0
  21. package/src/components/network-map-model.ts +255 -0
  22. package/src/components/useTimelineRouteData.ts +105 -235
  23. package/src/lib/analysis-runtime.ts +238 -0
  24. package/src/lib/api-client.ts +16 -215
  25. package/src/lib/api-contracts.ts +328 -0
  26. package/src/lib/archive-import-plan.ts +102 -0
  27. package/src/lib/archive-import.ts +170 -239
  28. package/src/lib/authored-live.ts +75 -120
  29. package/src/lib/backup.ts +335 -424
  30. package/src/lib/blocks-write.ts +30 -26
  31. package/src/lib/blocks.ts +18 -20
  32. package/src/lib/database-metrics.ts +88 -0
  33. package/src/lib/database-migrations.ts +34 -0
  34. package/src/lib/database-schema.ts +312 -0
  35. package/src/lib/database-writer.ts +69 -0
  36. package/src/lib/db.ts +84 -330
  37. package/src/lib/dm-read-model.ts +533 -0
  38. package/src/lib/dms-live.ts +34 -97
  39. package/src/lib/follow-graph.ts +17 -27
  40. package/src/lib/import-repository.ts +138 -0
  41. package/src/lib/inbox.ts +2 -1
  42. package/src/lib/live-sync-engine.ts +209 -0
  43. package/src/lib/live-transport-gateway.ts +128 -0
  44. package/src/lib/mention-threads-live.ts +90 -177
  45. package/src/lib/mentions-export.ts +1 -1
  46. package/src/lib/mentions-live.ts +57 -181
  47. package/src/lib/moderation-target.ts +15 -4
  48. package/src/lib/moderation-write.ts +1 -1
  49. package/src/lib/mutes-write.ts +30 -26
  50. package/src/lib/openai-response-runtime.ts +251 -0
  51. package/src/lib/paginated-sync.ts +93 -0
  52. package/src/lib/period-digest.ts +116 -304
  53. package/src/lib/profile-analysis.ts +36 -110
  54. package/src/lib/queries.ts +6 -2381
  55. package/src/lib/query-actions.ts +437 -0
  56. package/src/lib/query-client.tsx +47 -0
  57. package/src/lib/query-read-model-shared.ts +52 -0
  58. package/src/lib/query-read-models.ts +5 -0
  59. package/src/lib/query-resource.ts +41 -0
  60. package/src/lib/query-status.ts +164 -0
  61. package/src/lib/research.ts +1 -1
  62. package/src/lib/runtime-services.ts +20 -0
  63. package/src/lib/search-discussion.ts +75 -279
  64. package/src/lib/server-runtime-services.ts +30 -0
  65. package/src/lib/sqlite.ts +48 -12
  66. package/src/lib/streaming-ingestion.ts +240 -0
  67. package/src/lib/sync-cache.ts +6 -1
  68. package/src/lib/sync-plan.ts +175 -0
  69. package/src/lib/timeline-collections-live.ts +83 -257
  70. package/src/lib/timeline-live.ts +86 -236
  71. package/src/lib/timeline-read-model.ts +1191 -0
  72. package/src/lib/tweet-repository.ts +156 -0
  73. package/src/lib/tweet-search-live.ts +63 -167
  74. package/src/lib/web-sync.ts +67 -50
  75. package/src/lib/whois.ts +2 -1
  76. package/src/routes/__root.tsx +11 -8
  77. package/src/routes/api/action.tsx +1 -1
  78. package/src/routes/api/conversation.tsx +1 -1
  79. package/src/routes/api/query.tsx +32 -26
  80. package/src/routes/api/status.tsx +6 -4
  81. package/src/routes/api/sync.tsx +5 -2
  82. package/src/routes/blocks.tsx +97 -131
  83. package/src/routes/data-sources.tsx +17 -25
  84. package/src/routes/dms.tsx +167 -184
  85. package/src/routes/inbox.tsx +63 -57
  86. package/src/routes/links.tsx +31 -394
  87. package/src/routes/network-map.tsx +41 -344
  88. package/src/routes/rate-limits.tsx +17 -21
  89. package/src/lib/client-cache.ts +0 -109
@@ -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
+ }