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.
- package/CHANGELOG.md +16 -0
- package/package.json +2 -1
- package/src/cli/command-context.ts +17 -0
- package/src/cli/register-analysis.ts +500 -0
- package/src/cli/register-compose.ts +40 -0
- package/src/cli/register-graph.ts +132 -0
- package/src/cli/register-inbox.ts +41 -0
- package/src/cli/register-storage.ts +106 -0
- package/src/cli.ts +30 -750
- package/src/components/AccountSwitcher.tsx +7 -15
- package/src/components/AvatarChip.tsx +1 -1
- package/src/components/AvatarPreload.ts +149 -0
- package/src/components/MarkdownCitations.tsx +680 -0
- package/src/components/MarkdownViewer.tsx +8 -674
- package/src/components/ProfileAnalysisClient.ts +191 -0
- package/src/components/ProfileAnalysisStream.tsx +16 -185
- package/src/components/ProfilePreview.tsx +2 -0
- package/src/components/links-controller.ts +162 -0
- package/src/components/links-model.ts +198 -0
- package/src/components/network-map-controller.ts +84 -0
- package/src/components/network-map-model.ts +255 -0
- package/src/components/useTimelineRouteData.ts +105 -235
- package/src/lib/analysis-runtime.ts +238 -0
- package/src/lib/api-client.ts +16 -215
- package/src/lib/api-contracts.ts +328 -0
- package/src/lib/archive-import-plan.ts +102 -0
- package/src/lib/archive-import.ts +170 -239
- package/src/lib/authored-live.ts +75 -120
- package/src/lib/backup.ts +335 -424
- package/src/lib/blocks-write.ts +30 -26
- package/src/lib/blocks.ts +18 -20
- package/src/lib/database-metrics.ts +88 -0
- package/src/lib/database-migrations.ts +34 -0
- package/src/lib/database-schema.ts +312 -0
- package/src/lib/database-writer.ts +69 -0
- package/src/lib/db.ts +84 -330
- package/src/lib/dm-read-model.ts +533 -0
- package/src/lib/dms-live.ts +34 -97
- package/src/lib/follow-graph.ts +17 -27
- package/src/lib/import-repository.ts +138 -0
- package/src/lib/inbox.ts +2 -1
- package/src/lib/live-sync-engine.ts +209 -0
- package/src/lib/live-transport-gateway.ts +128 -0
- package/src/lib/mention-threads-live.ts +90 -177
- package/src/lib/mentions-export.ts +1 -1
- package/src/lib/mentions-live.ts +57 -181
- package/src/lib/moderation-target.ts +15 -4
- package/src/lib/moderation-write.ts +1 -1
- package/src/lib/mutes-write.ts +30 -26
- package/src/lib/openai-response-runtime.ts +251 -0
- package/src/lib/paginated-sync.ts +93 -0
- package/src/lib/period-digest.ts +116 -304
- package/src/lib/profile-analysis.ts +36 -110
- package/src/lib/queries.ts +6 -2381
- package/src/lib/query-actions.ts +437 -0
- package/src/lib/query-client.tsx +47 -0
- package/src/lib/query-read-model-shared.ts +52 -0
- package/src/lib/query-read-models.ts +5 -0
- package/src/lib/query-resource.ts +41 -0
- package/src/lib/query-status.ts +164 -0
- package/src/lib/research.ts +1 -1
- package/src/lib/runtime-services.ts +20 -0
- package/src/lib/search-discussion.ts +75 -279
- package/src/lib/server-runtime-services.ts +30 -0
- package/src/lib/sqlite.ts +48 -12
- package/src/lib/streaming-ingestion.ts +240 -0
- package/src/lib/sync-cache.ts +6 -1
- package/src/lib/sync-plan.ts +175 -0
- package/src/lib/timeline-collections-live.ts +83 -257
- package/src/lib/timeline-live.ts +86 -236
- package/src/lib/timeline-read-model.ts +1191 -0
- package/src/lib/tweet-repository.ts +156 -0
- package/src/lib/tweet-search-live.ts +63 -167
- package/src/lib/web-sync.ts +67 -50
- package/src/lib/whois.ts +2 -1
- package/src/routes/__root.tsx +11 -8
- package/src/routes/api/action.tsx +1 -1
- package/src/routes/api/conversation.tsx +1 -1
- package/src/routes/api/query.tsx +32 -26
- package/src/routes/api/status.tsx +6 -4
- package/src/routes/api/sync.tsx +5 -2
- package/src/routes/blocks.tsx +97 -131
- package/src/routes/data-sources.tsx +17 -25
- package/src/routes/dms.tsx +167 -184
- package/src/routes/inbox.tsx +63 -57
- package/src/routes/links.tsx +31 -394
- package/src/routes/network-map.tsx +41 -344
- package/src/routes/rate-limits.tsx +17 -21
- 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
|
+
}
|