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
@@ -1,6 +1,13 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { Effect } from "effect";
3
3
  import { z } from "zod";
4
+ import {
5
+ createAnalysisRequestBody,
6
+ extractOpenAIResponseText,
7
+ parseHybridAnalysis,
8
+ requestHybridAnalysisEffect,
9
+ resolveAnalysisModelSettings,
10
+ } from "./analysis-runtime";
4
11
  import { getNativeDb } from "./db";
5
12
  import { runEffectPromise, tryPromise } from "./effect-runtime";
6
13
  import { buildMediaJsonFromIncludes, countTweetMedia } from "./media-includes";
@@ -138,9 +145,6 @@ export type ProfileAnalysisStreamEvent =
138
145
  | { type: "done"; result: ProfileAnalysisRunResult }
139
146
  | { type: "error"; error: string };
140
147
 
141
- const DEFAULT_MODEL = "gpt-5.5";
142
- const DEFAULT_REASONING_EFFORT = "medium";
143
- const DEFAULT_SERVICE_TIER = "priority";
144
148
  const DEFAULT_MAX_TWEETS = 10_000;
145
149
  const DEFAULT_MAX_PAGES = 100;
146
150
  const DEFAULT_MAX_CONVERSATIONS = 80;
@@ -287,27 +291,15 @@ function resolveAccount(db: Database, accountId?: string) {
287
291
  }
288
292
 
289
293
  function modelFromOptions(options: ProfileAnalysisOptions) {
290
- return options.model ?? process.env.BIRDCLAW_AI_MODEL ?? DEFAULT_MODEL;
294
+ return resolveAnalysisModelSettings(options).model;
291
295
  }
292
296
 
293
297
  function reasoningEffortFromOptions(options: ProfileAnalysisOptions) {
294
- return (
295
- options.reasoningEffort ??
296
- (process.env.BIRDCLAW_OPENAI_REASONING_EFFORT as
297
- | ProfileAnalysisOptions["reasoningEffort"]
298
- | undefined) ??
299
- DEFAULT_REASONING_EFFORT
300
- );
298
+ return resolveAnalysisModelSettings(options).reasoningEffort;
301
299
  }
302
300
 
303
301
  function serviceTierFromOptions(options: ProfileAnalysisOptions) {
304
- return (
305
- options.serviceTier ??
306
- (process.env.BIRDCLAW_OPENAI_SERVICE_TIER as
307
- | ProfileAnalysisOptions["serviceTier"]
308
- | undefined) ??
309
- DEFAULT_SERVICE_TIER
310
- );
302
+ return resolveAnalysisModelSettings(options).serviceTier;
311
303
  }
312
304
 
313
305
  function tweetUrl(handle: string, id: string) {
@@ -1092,66 +1084,30 @@ function parseAnalysisFromHybridText(
1092
1084
  context: ProfileAnalysisContext,
1093
1085
  rawText: string,
1094
1086
  ): { analysis: ProfileAnalysis; markdown: string } {
1095
- const [markdownPart, jsonPart] = rawText.split(DELIMITER_PATTERN);
1096
- const markdown = (markdownPart ?? rawText).trim();
1097
- const candidate = jsonPart?.slice(
1098
- jsonPart.indexOf("{"),
1099
- jsonPart.lastIndexOf("}") + 1,
1100
- );
1101
- if (candidate?.startsWith("{")) {
1102
- try {
1103
- return {
1104
- markdown,
1105
- analysis: ProfileAnalysisSchema.parse(JSON.parse(candidate)),
1106
- };
1107
- } catch {
1108
- return { markdown, analysis: fallbackAnalysis(context, markdown) };
1109
- }
1110
- }
1111
- return { markdown, analysis: fallbackAnalysis(context, markdown) };
1087
+ const parsed = parseHybridAnalysis({
1088
+ rawText,
1089
+ parse: (value) => ProfileAnalysisSchema.parse(value),
1090
+ fallback: (markdown) => fallbackAnalysis(context, markdown),
1091
+ delimiterPattern: DELIMITER_PATTERN,
1092
+ });
1093
+ return { markdown: parsed.markdown, analysis: parsed.value };
1112
1094
  }
1113
1095
 
1114
1096
  function extractResponseText(payload: Record<string, unknown>) {
1115
- if (typeof payload.output_text === "string") {
1116
- return payload.output_text;
1117
- }
1118
- const output = Array.isArray(payload.output) ? payload.output : [];
1119
- const parts: string[] = [];
1120
- for (const item of output) {
1121
- if (!item || typeof item !== "object") continue;
1122
- const content = (item as { content?: unknown }).content;
1123
- if (!Array.isArray(content)) continue;
1124
- for (const block of content) {
1125
- if (!block || typeof block !== "object") continue;
1126
- const record = block as Record<string, unknown>;
1127
- if (typeof record.text === "string") parts.push(record.text);
1128
- }
1129
- }
1130
- return parts.join("");
1097
+ return extractOpenAIResponseText(payload);
1131
1098
  }
1132
1099
 
1133
1100
  function createOpenAIRequestBody(
1134
1101
  context: ProfileAnalysisContext,
1135
1102
  options: ProfileAnalysisOptions,
1136
1103
  ) {
1137
- return {
1138
- model: modelFromOptions(options),
1139
- reasoning: { effort: reasoningEffortFromOptions(options) },
1140
- service_tier: serviceTierFromOptions(options),
1141
- store: false,
1142
- max_output_tokens: 7000,
1143
- input: [
1144
- {
1145
- role: "system",
1146
- content:
1147
- "You are a precise X/Twitter profile analyst. Use only supplied data. Return Markdown plus the requested JSON after the delimiter.",
1148
- },
1149
- {
1150
- role: "user",
1151
- content: buildPrompt(context),
1152
- },
1153
- ],
1154
- };
1104
+ return createAnalysisRequestBody({
1105
+ settings: resolveAnalysisModelSettings(options),
1106
+ system:
1107
+ "You are a precise X/Twitter profile analyst. Use only supplied data. Return Markdown plus the requested JSON after the delimiter.",
1108
+ prompt: buildPrompt(context),
1109
+ stream: false,
1110
+ });
1155
1111
  }
1156
1112
 
1157
1113
  export function streamProfileAnalysisEffect(
@@ -1192,49 +1148,19 @@ export function streamProfileAnalysisEffect(
1192
1148
  return result;
1193
1149
  }
1194
1150
 
1195
- const apiKey = process.env.OPENAI_API_KEY;
1196
- if (!apiKey) {
1197
- return yield* Effect.fail(new Error("OPENAI_API_KEY is not set"));
1198
- }
1199
1151
  handlers.onEvent?.({ type: "start", context, cached: false });
1200
1152
  emitStatus(handlers, "Summarizing with AI", modelFromOptions(options));
1201
- const response = yield* tryProfilePromise(() =>
1202
- fetch("https://api.openai.com/v1/responses", {
1203
- method: "POST",
1204
- signal: options.signal,
1205
- headers: {
1206
- authorization: `Bearer ${apiKey}`,
1207
- "content-type": "application/json",
1208
- },
1209
- body: JSON.stringify(createOpenAIRequestBody(context, options)),
1210
- }),
1211
- );
1212
- if (!response.ok) {
1213
- const text = yield* tryProfilePromise(() => response.text());
1214
- return yield* Effect.fail(
1215
- new Error(
1216
- `OpenAI request failed: ${String(response.status)} ${text.slice(
1217
- 0,
1218
- 400,
1219
- )}`,
1220
- ),
1221
- );
1222
- }
1223
- const payload = (yield* tryProfilePromise(() => response.json())) as Record<
1224
- string,
1225
- unknown
1226
- >;
1227
- const rawText = extractResponseText(payload);
1228
- if (!rawText) {
1229
- return yield* Effect.fail(new Error("OpenAI returned no output text"));
1230
- }
1231
- const parsed = yield* tryProfileSync(() =>
1232
- parseAnalysisFromHybridText(context, rawText),
1233
- );
1153
+ const analysisResponse = yield* requestHybridAnalysisEffect({
1154
+ body: createOpenAIRequestBody(context, options),
1155
+ signal: options.signal,
1156
+ parse: (value) => ProfileAnalysisSchema.parse(value),
1157
+ fallback: (markdown) => fallbackAnalysis(context, markdown),
1158
+ delimiterPattern: DELIMITER_PATTERN,
1159
+ });
1234
1160
  const updatedAt = yield* tryProfileSync(() =>
1235
1161
  writeSyncCache(resultCacheKey(context, options), {
1236
- analysis: parsed.analysis,
1237
- markdown: parsed.markdown,
1162
+ analysis: analysisResponse.value,
1163
+ markdown: analysisResponse.markdown,
1238
1164
  model: modelFromOptions(options),
1239
1165
  reasoningEffort: reasoningEffortFromOptions(options),
1240
1166
  serviceTier: serviceTierFromOptions(options),
@@ -1242,8 +1168,8 @@ export function streamProfileAnalysisEffect(
1242
1168
  );
1243
1169
  const result: ProfileAnalysisRunResult = {
1244
1170
  context,
1245
- analysis: parsed.analysis,
1246
- markdown: parsed.markdown,
1171
+ analysis: analysisResponse.value,
1172
+ markdown: analysisResponse.markdown,
1247
1173
  model: modelFromOptions(options),
1248
1174
  reasoningEffort: reasoningEffortFromOptions(options),
1249
1175
  serviceTier: serviceTierFromOptions(options),