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,238 @@
1
+ import { Effect } from "effect";
2
+ import { tryPromise } from "./effect-runtime";
3
+ import {
4
+ readOpenAIResponseStreamEffect,
5
+ requestOpenAIResponseEffect,
6
+ } from "./openai-response-runtime";
7
+ import {
8
+ defaultRuntimeServices,
9
+ type RuntimeServices,
10
+ } from "./runtime-services";
11
+
12
+ const DEFAULT_MODEL = "gpt-5.5";
13
+ const DEFAULT_REASONING_EFFORT = "medium";
14
+ const DEFAULT_SERVICE_TIER = "priority";
15
+ const DEFAULT_DELIMITER_PATTERN = /\n---\s*\n/;
16
+
17
+ export type AnalysisReasoningEffort = "minimal" | "low" | "medium" | "high";
18
+ export type AnalysisServiceTier = "default" | "flex" | "priority";
19
+
20
+ export interface AnalysisModelOptions {
21
+ model?: string;
22
+ reasoningEffort?: AnalysisReasoningEffort;
23
+ serviceTier?: AnalysisServiceTier;
24
+ }
25
+
26
+ export interface AnalysisModelSettings {
27
+ model: string;
28
+ reasoningEffort: AnalysisReasoningEffort;
29
+ serviceTier: AnalysisServiceTier;
30
+ }
31
+
32
+ export interface HybridAnalysisResult<T> {
33
+ value: T;
34
+ markdown: string;
35
+ rawText: string;
36
+ responseId?: string;
37
+ usage?: unknown;
38
+ }
39
+
40
+ function toError(error: unknown) {
41
+ return error instanceof Error ? error : new Error(String(error));
42
+ }
43
+
44
+ export function resolveAnalysisModelSettings(
45
+ options: AnalysisModelOptions,
46
+ runtime: RuntimeServices = defaultRuntimeServices,
47
+ ): AnalysisModelSettings {
48
+ return {
49
+ model: options.model ?? runtime.env("BIRDCLAW_AI_MODEL") ?? DEFAULT_MODEL,
50
+ reasoningEffort:
51
+ options.reasoningEffort ??
52
+ (runtime.env(
53
+ "BIRDCLAW_OPENAI_REASONING_EFFORT",
54
+ ) as AnalysisReasoningEffort) ??
55
+ DEFAULT_REASONING_EFFORT,
56
+ serviceTier:
57
+ options.serviceTier ??
58
+ (runtime.env("BIRDCLAW_OPENAI_SERVICE_TIER") as AnalysisServiceTier) ??
59
+ DEFAULT_SERVICE_TIER,
60
+ };
61
+ }
62
+
63
+ export function createAnalysisRequestBody({
64
+ settings,
65
+ system,
66
+ prompt,
67
+ stream,
68
+ maxOutputTokens = 7000,
69
+ }: {
70
+ settings: AnalysisModelSettings;
71
+ system: string;
72
+ prompt: string;
73
+ stream: boolean;
74
+ maxOutputTokens?: number;
75
+ }) {
76
+ return {
77
+ model: settings.model,
78
+ reasoning: { effort: settings.reasoningEffort },
79
+ service_tier: settings.serviceTier,
80
+ store: false,
81
+ ...(stream ? { stream: true } : {}),
82
+ max_output_tokens: maxOutputTokens,
83
+ input: [
84
+ { role: "system", content: system },
85
+ { role: "user", content: prompt },
86
+ ],
87
+ };
88
+ }
89
+
90
+ export function parseHybridAnalysis<T>({
91
+ rawText,
92
+ parse,
93
+ fallback,
94
+ delimiterPattern = DEFAULT_DELIMITER_PATTERN,
95
+ }: {
96
+ rawText: string;
97
+ parse: (value: unknown) => T;
98
+ fallback: (markdown: string) => T;
99
+ delimiterPattern?: RegExp;
100
+ }) {
101
+ const [markdownPart, jsonPart] = rawText.split(delimiterPattern);
102
+ const markdown = (markdownPart ?? rawText).trim();
103
+ const candidate = jsonPart?.slice(
104
+ jsonPart.indexOf("{"),
105
+ jsonPart.lastIndexOf("}") + 1,
106
+ );
107
+ if (candidate?.startsWith("{")) {
108
+ try {
109
+ return { markdown, value: parse(JSON.parse(candidate)) };
110
+ } catch {
111
+ return { markdown, value: fallback(markdown) };
112
+ }
113
+ }
114
+ return { markdown, value: fallback(markdown) };
115
+ }
116
+
117
+ export function extractOpenAIResponseText(payload: Record<string, unknown>) {
118
+ if (typeof payload.output_text === "string") {
119
+ return payload.output_text;
120
+ }
121
+ const output = Array.isArray(payload.output) ? payload.output : [];
122
+ const parts: string[] = [];
123
+ for (const item of output) {
124
+ if (!item || typeof item !== "object") continue;
125
+ const content = (item as { content?: unknown }).content;
126
+ if (!Array.isArray(content)) continue;
127
+ for (const block of content) {
128
+ if (!block || typeof block !== "object") continue;
129
+ const record = block as Record<string, unknown>;
130
+ if (typeof record.text === "string") parts.push(record.text);
131
+ }
132
+ }
133
+ return parts.join("");
134
+ }
135
+
136
+ export function readHybridAnalysisStreamEffect<T>(
137
+ response: Response,
138
+ {
139
+ parse,
140
+ fallback,
141
+ onDelta,
142
+ delimiterPattern = DEFAULT_DELIMITER_PATTERN,
143
+ }: {
144
+ parse: (value: unknown) => T;
145
+ fallback: (markdown: string) => T;
146
+ onDelta?: (delta: string) => void;
147
+ delimiterPattern?: RegExp;
148
+ },
149
+ ): Effect.Effect<HybridAnalysisResult<T>, Error> {
150
+ return Effect.gen(function* () {
151
+ const stream = yield* readOpenAIResponseStreamEffect(response, {
152
+ delimiterPattern,
153
+ onDelta,
154
+ });
155
+ const parsed = parseHybridAnalysis({
156
+ rawText: stream.rawText,
157
+ parse,
158
+ fallback,
159
+ delimiterPattern,
160
+ });
161
+ return {
162
+ ...parsed,
163
+ rawText: stream.rawText,
164
+ ...(stream.responseId ? { responseId: stream.responseId } : {}),
165
+ ...(stream.usage === undefined ? {} : { usage: stream.usage }),
166
+ };
167
+ });
168
+ }
169
+
170
+ export function streamHybridAnalysisEffect<T>({
171
+ body,
172
+ signal,
173
+ runtime = defaultRuntimeServices,
174
+ parse,
175
+ fallback,
176
+ onDelta,
177
+ delimiterPattern = DEFAULT_DELIMITER_PATTERN,
178
+ }: {
179
+ body: unknown;
180
+ signal?: AbortSignal;
181
+ runtime?: RuntimeServices;
182
+ parse: (value: unknown) => T;
183
+ fallback: (markdown: string) => T;
184
+ onDelta?: (delta: string) => void;
185
+ delimiterPattern?: RegExp;
186
+ }): Effect.Effect<HybridAnalysisResult<T>, Error> {
187
+ return Effect.gen(function* () {
188
+ const response = yield* requestOpenAIResponseEffect({
189
+ body,
190
+ signal,
191
+ runtime,
192
+ });
193
+ return yield* readHybridAnalysisStreamEffect(response, {
194
+ parse,
195
+ fallback,
196
+ onDelta,
197
+ delimiterPattern,
198
+ });
199
+ });
200
+ }
201
+
202
+ export function requestHybridAnalysisEffect<T>({
203
+ body,
204
+ signal,
205
+ runtime = defaultRuntimeServices,
206
+ parse,
207
+ fallback,
208
+ delimiterPattern = DEFAULT_DELIMITER_PATTERN,
209
+ }: {
210
+ body: unknown;
211
+ signal?: AbortSignal;
212
+ runtime?: RuntimeServices;
213
+ parse: (value: unknown) => T;
214
+ fallback: (markdown: string) => T;
215
+ delimiterPattern?: RegExp;
216
+ }): Effect.Effect<HybridAnalysisResult<T>, Error> {
217
+ return Effect.gen(function* () {
218
+ const response = yield* requestOpenAIResponseEffect({
219
+ body,
220
+ signal,
221
+ runtime,
222
+ });
223
+ const payload = (yield* tryPromise(() => response.json()).pipe(
224
+ Effect.mapError(toError),
225
+ )) as Record<string, unknown>;
226
+ const rawText = extractOpenAIResponseText(payload);
227
+ if (!rawText) {
228
+ return yield* Effect.fail(new Error("OpenAI returned no output text"));
229
+ }
230
+ const parsed = parseHybridAnalysis({
231
+ rawText,
232
+ parse,
233
+ fallback,
234
+ delimiterPattern,
235
+ });
236
+ return { ...parsed, rawText };
237
+ });
238
+ }
@@ -1,109 +1,21 @@
1
1
  import { Data, Effect } from "effect";
2
2
  import { z } from "zod";
3
+ import {
4
+ actionResponseSchema,
5
+ queryEnvelopeSchema,
6
+ queryResponseSchema,
7
+ webSyncJobSchema,
8
+ } from "./api-contracts";
3
9
  import { runEffectPromise } from "./effect-runtime";
4
- import type {
5
- DmConversationItem,
6
- DmMessageItem,
7
- QueryEnvelope,
8
- QueryResponse,
9
- TimelineItem,
10
- } from "./types";
10
+ import {
11
+ defaultRuntimeServices,
12
+ type RuntimeServices,
13
+ } from "./runtime-services";
11
14
  import type {
12
15
  WebSyncJobSnapshot,
13
16
  WebSyncKind,
14
17
  WebSyncOptions,
15
- WebSyncResponse,
16
18
  } from "./web-sync";
17
-
18
- const jsonRecordSchema = z.object({}).passthrough();
19
- const resourceKindSchema = z.enum(["home", "mentions", "authored", "dms"]);
20
- const webSyncKindSchema = z.enum([
21
- "timeline",
22
- "mentions",
23
- "likes",
24
- "bookmarks",
25
- "dms",
26
- ]);
27
-
28
- const queryEnvelopeSchema = z
29
- .object({
30
- accounts: z.array(jsonRecordSchema),
31
- archives: z.array(jsonRecordSchema),
32
- transport: z
33
- .object({
34
- statusText: z.string(),
35
- })
36
- .passthrough(),
37
- stats: z.object({
38
- home: z.number(),
39
- mentions: z.number(),
40
- dms: z.number(),
41
- needsReply: z.number(),
42
- inbox: z.number(),
43
- }),
44
- })
45
- .transform((value) => value as unknown as QueryEnvelope);
46
-
47
- const queryResponseSchema = z
48
- .object({
49
- resource: resourceKindSchema,
50
- items: z.array(jsonRecordSchema),
51
- selectedConversation: z
52
- .object({
53
- conversation: jsonRecordSchema,
54
- messages: z.array(jsonRecordSchema),
55
- })
56
- .nullish(),
57
- })
58
- .transform(
59
- (value) =>
60
- ({
61
- ...value,
62
- items: value.items as unknown as Array<
63
- TimelineItem | DmConversationItem
64
- >,
65
- selectedConversation: value.selectedConversation
66
- ? {
67
- conversation: value.selectedConversation
68
- .conversation as unknown as DmConversationItem,
69
- messages: value.selectedConversation
70
- .messages as unknown as DmMessageItem[],
71
- }
72
- : value.selectedConversation,
73
- }) as QueryResponse,
74
- );
75
-
76
- const webSyncResponseSchema = z
77
- .object({
78
- ok: z.boolean(),
79
- kind: webSyncKindSchema,
80
- accountId: z.string().optional(),
81
- summary: z.string(),
82
- steps: z.array(jsonRecordSchema),
83
- startedAt: z.string().optional(),
84
- finishedAt: z.string().optional(),
85
- inProgress: z.boolean().optional(),
86
- backup: z.unknown().optional(),
87
- error: z.string().optional(),
88
- })
89
- .transform((value) => value as unknown as WebSyncResponse);
90
-
91
- const webSyncJobSchema = z
92
- .object({
93
- id: z.string(),
94
- kind: webSyncKindSchema,
95
- accountId: z.string().optional(),
96
- status: z.enum(["running", "succeeded", "failed"]),
97
- startedAt: z.string(),
98
- finishedAt: z.string().optional(),
99
- summary: z.string(),
100
- inProgress: z.boolean(),
101
- result: webSyncResponseSchema.optional(),
102
- error: z.string().optional(),
103
- })
104
- .transform((value) => value as unknown as WebSyncJobSnapshot);
105
-
106
- const actionResponseSchema = jsonRecordSchema;
107
19
  const SYNC_POLL_INTERVAL_MS = 500;
108
20
 
109
21
  export class ApiFetchError extends Data.TaggedError("ApiFetchError")<{
@@ -153,10 +65,11 @@ export function fetchJsonEffect<T>(
153
65
  init: RequestInit | undefined,
154
66
  schema: z.ZodType<T>,
155
67
  fallbackMessage: string,
68
+ runtime: RuntimeServices = defaultRuntimeServices,
156
69
  ) {
157
70
  return Effect.gen(function* () {
158
71
  const response = yield* Effect.tryPromise({
159
- try: () => fetch(input, init),
72
+ try: () => runtime.fetch(input, init),
160
73
  catch: (cause) => apiFetchErrorFromCause(cause, fallbackMessage),
161
74
  });
162
75
  const data = yield* readJsonEffect(response);
@@ -187,8 +100,11 @@ export function fetchJson<T>(
187
100
  init: RequestInit | undefined,
188
101
  schema: z.ZodType<T>,
189
102
  fallbackMessage: string,
103
+ runtime: RuntimeServices = defaultRuntimeServices,
190
104
  ): Promise<T> {
191
- return runApiEffect(fetchJsonEffect(input, init, schema, fallbackMessage));
105
+ return runApiEffect(
106
+ fetchJsonEffect(input, init, schema, fallbackMessage, runtime),
107
+ );
192
108
  }
193
109
 
194
110
  export function fetchQueryEnvelope(init?: RequestInit) {
@@ -0,0 +1,328 @@
1
+ import { z } from "zod";
2
+ import type {
3
+ AccountRecord,
4
+ ArchiveCandidate,
5
+ DmConversationItem,
6
+ DmMessageItem,
7
+ DmSearchMatchItem,
8
+ EmbeddedTweet,
9
+ ProfileAffiliation,
10
+ ProfileRecord,
11
+ QueryEnvelope,
12
+ QueryResponse,
13
+ TimelineItem,
14
+ TransportStatus,
15
+ TweetEntities,
16
+ TweetMediaItem,
17
+ UrlExpansionItem,
18
+ } from "./types";
19
+ import type {
20
+ WebSyncJobSnapshot,
21
+ WebSyncResponse,
22
+ WebSyncStep,
23
+ } from "./web-sync";
24
+
25
+ const jsonRecordSchema = z.record(z.string(), z.unknown());
26
+ const unknownProfile: ProfileRecord = {
27
+ id: "profile_unknown",
28
+ handle: "unknown",
29
+ displayName: "Unknown",
30
+ bio: "",
31
+ followersCount: 0,
32
+ avatarHue: 0,
33
+ createdAt: "",
34
+ };
35
+
36
+ const profileAffiliationSchema: z.ZodType<ProfileAffiliation> = z.object({
37
+ organizationProfileId: z.string(),
38
+ organizationName: z.string().optional(),
39
+ organizationHandle: z.string().optional(),
40
+ badgeUrl: z.string().nullable().optional(),
41
+ url: z.string().nullable().optional(),
42
+ label: z.string().nullable().optional(),
43
+ source: z.string(),
44
+ firstSeenAt: z.string(),
45
+ lastSeenAt: z.string(),
46
+ isActive: z.boolean(),
47
+ });
48
+
49
+ const profileRecordSchema = z.object({
50
+ id: z.string().default("profile_unknown"),
51
+ handle: z.string().default("unknown"),
52
+ displayName: z.string().default("Unknown"),
53
+ bio: z.string().default(""),
54
+ followersCount: z.number().default(0),
55
+ followingCount: z.number().optional(),
56
+ avatarHue: z.number().default(0),
57
+ avatarUrl: z.string().optional(),
58
+ location: z.string().optional(),
59
+ url: z.string().optional(),
60
+ verifiedType: z.string().optional(),
61
+ entities: jsonRecordSchema.optional(),
62
+ affiliations: z.array(profileAffiliationSchema).optional(),
63
+ primaryAffiliation: profileAffiliationSchema.optional(),
64
+ createdAt: z.string().default(""),
65
+ }) satisfies z.ZodType<ProfileRecord>;
66
+
67
+ const tweetEntitiesSchema: z.ZodType<TweetEntities> = z.object({
68
+ mentions: z
69
+ .array(
70
+ z.object({
71
+ username: z.string(),
72
+ id: z.string().optional(),
73
+ start: z.number(),
74
+ end: z.number(),
75
+ profile: profileRecordSchema.optional(),
76
+ }),
77
+ )
78
+ .optional(),
79
+ urls: z
80
+ .array(
81
+ z.object({
82
+ url: z.string(),
83
+ expandedUrl: z.string(),
84
+ displayUrl: z.string(),
85
+ start: z.number(),
86
+ end: z.number(),
87
+ title: z.string().optional(),
88
+ description: z.string().nullable().optional(),
89
+ imageUrl: z.string().nullable().optional(),
90
+ siteName: z.string().nullable().optional(),
91
+ }),
92
+ )
93
+ .optional(),
94
+ hashtags: z
95
+ .array(
96
+ z.object({
97
+ tag: z.string(),
98
+ start: z.number(),
99
+ end: z.number(),
100
+ }),
101
+ )
102
+ .optional(),
103
+ article: z
104
+ .object({
105
+ title: z.string(),
106
+ previewText: z.string().optional(),
107
+ url: z.string(),
108
+ coverImageUrl: z.string().optional(),
109
+ })
110
+ .optional(),
111
+ });
112
+
113
+ const tweetMediaSchema: z.ZodType<TweetMediaItem> = z.object({
114
+ url: z.string(),
115
+ type: z.enum(["image", "video", "gif", "unknown"]),
116
+ altText: z.string().optional(),
117
+ width: z.number().optional(),
118
+ height: z.number().optional(),
119
+ thumbnailUrl: z.string().optional(),
120
+ durationMs: z.number().optional(),
121
+ variants: z
122
+ .array(
123
+ z.object({
124
+ url: z.string(),
125
+ contentType: z.string().optional(),
126
+ bitRate: z.number().optional(),
127
+ }),
128
+ )
129
+ .optional(),
130
+ });
131
+
132
+ const embeddedTweetSchema: z.ZodType<EmbeddedTweet> = z.object({
133
+ id: z.string(),
134
+ text: z.string(),
135
+ createdAt: z.string().default(""),
136
+ replyToId: z.string().nullable().optional(),
137
+ isReplied: z.boolean().optional(),
138
+ likeCount: z.number().optional(),
139
+ mediaCount: z.number().optional(),
140
+ bookmarked: z.boolean().optional(),
141
+ liked: z.boolean().optional(),
142
+ author: profileRecordSchema.default(unknownProfile),
143
+ entities: tweetEntitiesSchema.default({}),
144
+ media: z.array(tweetMediaSchema).default([]),
145
+ });
146
+
147
+ const timelineItemSchema: z.ZodType<TimelineItem> = z.object({
148
+ id: z.string(),
149
+ accountId: z.string().default("acct_primary"),
150
+ accountHandle: z.string().default(""),
151
+ kind: z
152
+ .enum(["home", "mention", "authored", "search", "like", "bookmark"])
153
+ .default("home"),
154
+ text: z.string(),
155
+ searchSnippet: z.string().optional(),
156
+ createdAt: z.string().default(""),
157
+ replyToId: z.string().nullable().optional(),
158
+ isReplied: z.boolean().default(false),
159
+ likeCount: z.number().default(0),
160
+ mediaCount: z.number().default(0),
161
+ bookmarked: z.boolean().default(false),
162
+ liked: z.boolean().default(false),
163
+ author: profileRecordSchema.default(unknownProfile),
164
+ entities: tweetEntitiesSchema.default({}),
165
+ media: z.array(tweetMediaSchema).default([]),
166
+ replyToTweet: embeddedTweetSchema.nullable().optional(),
167
+ quotedTweet: embeddedTweetSchema.nullable().optional(),
168
+ retweetedTweet: embeddedTweetSchema.nullable().optional(),
169
+ qualityReason: z.string().nullable().optional(),
170
+ });
171
+
172
+ const dmMessageSchema: z.ZodType<DmMessageItem> = z.object({
173
+ id: z.string(),
174
+ conversationId: z.string().default(""),
175
+ text: z.string(),
176
+ createdAt: z.string().default(""),
177
+ direction: z.enum(["inbound", "outbound"]).default("inbound"),
178
+ isReplied: z.boolean().default(false),
179
+ mediaCount: z.number().default(0),
180
+ sender: profileRecordSchema.default(unknownProfile),
181
+ });
182
+
183
+ const urlExpansionSchema: z.ZodType<UrlExpansionItem> = z.object({
184
+ url: z.string(),
185
+ expandedUrl: z.string(),
186
+ finalUrl: z.string(),
187
+ status: z.enum(["hit", "miss", "error"]),
188
+ source: z.enum(["cache", "network"]),
189
+ title: z.string().optional(),
190
+ description: z.string().nullable().optional(),
191
+ error: z.string().optional(),
192
+ updatedAt: z.string(),
193
+ });
194
+
195
+ const dmSearchMatchSchema: z.ZodType<DmSearchMatchItem> = z.object({
196
+ message: dmMessageSchema,
197
+ before: z.array(dmMessageSchema),
198
+ after: z.array(dmMessageSchema),
199
+ urlExpansions: z.array(urlExpansionSchema).optional(),
200
+ });
201
+
202
+ const dmConversationSchema: z.ZodType<DmConversationItem> = z.object({
203
+ id: z.string(),
204
+ accountId: z.string(),
205
+ accountHandle: z.string().default(""),
206
+ title: z.string(),
207
+ searchSnippet: z.string().optional(),
208
+ inboxKind: z.enum(["accepted", "request"]).optional(),
209
+ isMessageRequest: z.boolean().optional(),
210
+ lastMessageAt: z.string().default(""),
211
+ lastMessagePreview: z.string().default(""),
212
+ unreadCount: z.number().default(0),
213
+ needsReply: z.boolean().default(false),
214
+ influenceScore: z.number().default(0),
215
+ influenceLabel: z.string().default(""),
216
+ participant: profileRecordSchema.default(unknownProfile),
217
+ matches: z.array(dmSearchMatchSchema).optional(),
218
+ });
219
+
220
+ const accountRecordSchema: z.ZodType<AccountRecord> = z.object({
221
+ id: z.string(),
222
+ name: z.string().default(""),
223
+ handle: z.string().default(""),
224
+ externalUserId: z.string().nullable().optional(),
225
+ profileId: z.string().optional(),
226
+ avatarHue: z.number().optional(),
227
+ avatarUrl: z.string().optional(),
228
+ transport: z.string().default("local"),
229
+ isDefault: z.coerce.number().default(0),
230
+ createdAt: z.string().default(""),
231
+ });
232
+
233
+ const archiveCandidateSchema: z.ZodType<ArchiveCandidate> = z.object({
234
+ path: z.string(),
235
+ name: z.string().default(""),
236
+ size: z.number().default(0),
237
+ sizeFormatted: z.string().default(""),
238
+ modifiedTime: z.string().default(""),
239
+ dateFormatted: z.string().default(""),
240
+ });
241
+
242
+ const transportStatusSchema: z.ZodType<TransportStatus> = z.object({
243
+ installed: z.boolean().default(false),
244
+ availableTransport: z.enum(["xurl", "local"]).default("local"),
245
+ statusText: z.string(),
246
+ rawStatus: z.string().optional(),
247
+ });
248
+
249
+ export const queryEnvelopeSchema: z.ZodType<QueryEnvelope> = z.object({
250
+ accounts: z.array(accountRecordSchema),
251
+ archives: z.array(archiveCandidateSchema),
252
+ transport: transportStatusSchema,
253
+ stats: z.object({
254
+ home: z.number(),
255
+ mentions: z.number(),
256
+ dms: z.number(),
257
+ needsReply: z.number(),
258
+ inbox: z.number(),
259
+ }),
260
+ });
261
+
262
+ const timelineQueryResponseSchema = z.object({
263
+ resource: z.enum(["home", "mentions", "authored", "search"]),
264
+ items: z.array(timelineItemSchema),
265
+ selectedConversation: z.undefined().optional(),
266
+ });
267
+
268
+ const dmQueryResponseSchema = z.object({
269
+ resource: z.literal("dms"),
270
+ items: z.array(dmConversationSchema),
271
+ selectedConversation: z
272
+ .object({
273
+ conversation: dmConversationSchema,
274
+ messages: z.array(dmMessageSchema),
275
+ })
276
+ .nullable()
277
+ .optional(),
278
+ });
279
+
280
+ export const queryResponseSchema: z.ZodType<QueryResponse> = z.union([
281
+ timelineQueryResponseSchema,
282
+ dmQueryResponseSchema,
283
+ ]);
284
+
285
+ export const webSyncKindSchema = z.enum([
286
+ "timeline",
287
+ "mentions",
288
+ "likes",
289
+ "bookmarks",
290
+ "dms",
291
+ ]);
292
+
293
+ const webSyncStepSchema: z.ZodType<WebSyncStep> = z.object({
294
+ kind: z.union([webSyncKindSchema, z.literal("mention-threads")]),
295
+ label: z.string(),
296
+ count: z.number(),
297
+ source: z.string().optional(),
298
+ partial: z.boolean().optional(),
299
+ warnings: z.array(z.string()).optional(),
300
+ });
301
+
302
+ export const webSyncResponseSchema: z.ZodType<WebSyncResponse> = z.object({
303
+ ok: z.boolean(),
304
+ kind: webSyncKindSchema,
305
+ accountId: z.string().optional(),
306
+ startedAt: z.string().default(""),
307
+ finishedAt: z.string().optional(),
308
+ summary: z.string(),
309
+ steps: z.array(webSyncStepSchema).default([]),
310
+ inProgress: z.boolean().optional(),
311
+ backup: z.custom<WebSyncResponse["backup"]>().optional(),
312
+ error: z.string().optional(),
313
+ });
314
+
315
+ export const webSyncJobSchema: z.ZodType<WebSyncJobSnapshot> = z.object({
316
+ id: z.string(),
317
+ kind: webSyncKindSchema,
318
+ accountId: z.string().optional(),
319
+ status: z.enum(["running", "succeeded", "failed"]),
320
+ startedAt: z.string(),
321
+ finishedAt: z.string().optional(),
322
+ summary: z.string(),
323
+ inProgress: z.boolean(),
324
+ result: webSyncResponseSchema.optional(),
325
+ error: z.string().optional(),
326
+ });
327
+
328
+ export const actionResponseSchema = jsonRecordSchema;