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,125 +1,22 @@
1
1
  import { Data, Effect } from "effect";
2
2
  import { z } from "zod";
3
3
  import {
4
- deleteClientCache,
5
- deleteClientCacheByPrefix,
6
- loadClientCache,
7
- readClientCache,
8
- } from "./client-cache";
4
+ actionResponseSchema,
5
+ queryEnvelopeSchema,
6
+ queryResponseSchema,
7
+ webSyncJobSchema,
8
+ } from "./api-contracts";
9
9
  import { runEffectPromise } from "./effect-runtime";
10
- import type {
11
- DmConversationItem,
12
- DmMessageItem,
13
- QueryEnvelope,
14
- QueryResponse,
15
- TimelineItem,
16
- } from "./types";
10
+ import {
11
+ defaultRuntimeServices,
12
+ type RuntimeServices,
13
+ } from "./runtime-services";
17
14
  import type {
18
15
  WebSyncJobSnapshot,
19
16
  WebSyncKind,
20
17
  WebSyncOptions,
21
- WebSyncResponse,
22
18
  } from "./web-sync";
23
-
24
- const jsonRecordSchema = z.object({}).passthrough();
25
- const resourceKindSchema = z.enum(["home", "mentions", "authored", "dms"]);
26
- const webSyncKindSchema = z.enum([
27
- "timeline",
28
- "mentions",
29
- "likes",
30
- "bookmarks",
31
- "dms",
32
- ]);
33
-
34
- const queryEnvelopeSchema = z
35
- .object({
36
- accounts: z.array(jsonRecordSchema),
37
- archives: z.array(jsonRecordSchema),
38
- transport: z
39
- .object({
40
- statusText: z.string(),
41
- })
42
- .passthrough(),
43
- stats: z.object({
44
- home: z.number(),
45
- mentions: z.number(),
46
- dms: z.number(),
47
- needsReply: z.number(),
48
- inbox: z.number(),
49
- }),
50
- })
51
- .transform((value) => value as unknown as QueryEnvelope);
52
-
53
- const queryResponseSchema = z
54
- .object({
55
- resource: resourceKindSchema,
56
- items: z.array(jsonRecordSchema),
57
- selectedConversation: z
58
- .object({
59
- conversation: jsonRecordSchema,
60
- messages: z.array(jsonRecordSchema),
61
- })
62
- .nullish(),
63
- })
64
- .transform(
65
- (value) =>
66
- ({
67
- ...value,
68
- items: value.items as unknown as Array<
69
- TimelineItem | DmConversationItem
70
- >,
71
- selectedConversation: value.selectedConversation
72
- ? {
73
- conversation: value.selectedConversation
74
- .conversation as unknown as DmConversationItem,
75
- messages: value.selectedConversation
76
- .messages as unknown as DmMessageItem[],
77
- }
78
- : value.selectedConversation,
79
- }) as QueryResponse,
80
- );
81
-
82
- const webSyncResponseSchema = z
83
- .object({
84
- ok: z.boolean(),
85
- kind: webSyncKindSchema,
86
- accountId: z.string().optional(),
87
- summary: z.string(),
88
- steps: z.array(jsonRecordSchema),
89
- startedAt: z.string().optional(),
90
- finishedAt: z.string().optional(),
91
- inProgress: z.boolean().optional(),
92
- backup: z.unknown().optional(),
93
- error: z.string().optional(),
94
- })
95
- .transform((value) => value as unknown as WebSyncResponse);
96
-
97
- const webSyncJobSchema = z
98
- .object({
99
- id: z.string(),
100
- kind: webSyncKindSchema,
101
- accountId: z.string().optional(),
102
- status: z.enum(["running", "succeeded", "failed"]),
103
- startedAt: z.string(),
104
- finishedAt: z.string().optional(),
105
- summary: z.string(),
106
- inProgress: z.boolean(),
107
- result: webSyncResponseSchema.optional(),
108
- error: z.string().optional(),
109
- })
110
- .transform((value) => value as unknown as WebSyncJobSnapshot);
111
-
112
- const actionResponseSchema = jsonRecordSchema;
113
19
  const SYNC_POLL_INTERVAL_MS = 500;
114
- const STATUS_CACHE_KEY = "api:/status";
115
- const QUERY_CACHE_PREFIX = "api:/query:";
116
- const STATUS_CACHE_MAX_AGE_MS = 60_000;
117
- const QUERY_CACHE_MAX_AGE_MS = 5 * 60_000;
118
-
119
- interface ClientFetchCacheOptions {
120
- force?: boolean;
121
- maxAgeMs?: number;
122
- }
123
20
 
124
21
  export class ApiFetchError extends Data.TaggedError("ApiFetchError")<{
125
22
  readonly message: string;
@@ -163,64 +60,16 @@ function runApiEffect<T, E>(effect: Effect.Effect<T, E>) {
163
60
  return runEffectPromise(effect);
164
61
  }
165
62
 
166
- function splitSignal(init?: RequestInit) {
167
- if (!init) return { requestInit: undefined, signal: undefined };
168
- const { signal, ...requestInit } = init;
169
- return { requestInit, signal: signal ?? undefined };
170
- }
171
-
172
- function waitForSignal<T>(promise: Promise<T>, signal?: AbortSignal) {
173
- if (!signal) return promise;
174
- if (signal.aborted) {
175
- return Promise.reject(
176
- new DOMException("The operation was aborted.", "AbortError"),
177
- );
178
- }
179
-
180
- return new Promise<T>((resolve, reject) => {
181
- const onAbort = () => {
182
- reject(new DOMException("The operation was aborted.", "AbortError"));
183
- };
184
- signal.addEventListener("abort", onAbort, { once: true });
185
- promise.then(
186
- (value) => {
187
- signal.removeEventListener("abort", onAbort);
188
- resolve(value);
189
- },
190
- (error: unknown) => {
191
- signal.removeEventListener("abort", onAbort);
192
- reject(error);
193
- },
194
- );
195
- });
196
- }
197
-
198
- function queryCacheKey(input: RequestInfo | URL) {
199
- const raw =
200
- typeof input === "string"
201
- ? input
202
- : input instanceof URL
203
- ? input.toString()
204
- : input.url;
205
- const base =
206
- typeof window === "undefined"
207
- ? "http://birdclaw.local"
208
- : window.location.origin;
209
- const url = new URL(raw, base);
210
- url.searchParams.delete("refresh");
211
- url.searchParams.sort();
212
- return `${QUERY_CACHE_PREFIX}${url.pathname}?${url.searchParams.toString()}`;
213
- }
214
-
215
63
  export function fetchJsonEffect<T>(
216
64
  input: RequestInfo | URL,
217
65
  init: RequestInit | undefined,
218
66
  schema: z.ZodType<T>,
219
67
  fallbackMessage: string,
68
+ runtime: RuntimeServices = defaultRuntimeServices,
220
69
  ) {
221
70
  return Effect.gen(function* () {
222
71
  const response = yield* Effect.tryPromise({
223
- try: () => fetch(input, init),
72
+ try: () => runtime.fetch(input, init),
224
73
  catch: (cause) => apiFetchErrorFromCause(cause, fallbackMessage),
225
74
  });
226
75
  const data = yield* readJsonEffect(response);
@@ -251,31 +100,15 @@ export function fetchJson<T>(
251
100
  init: RequestInit | undefined,
252
101
  schema: z.ZodType<T>,
253
102
  fallbackMessage: string,
103
+ runtime: RuntimeServices = defaultRuntimeServices,
254
104
  ): Promise<T> {
255
- return runApiEffect(fetchJsonEffect(input, init, schema, fallbackMessage));
256
- }
257
-
258
- export function readCachedQueryEnvelope() {
259
- return readClientCache<QueryEnvelope>(
260
- STATUS_CACHE_KEY,
261
- STATUS_CACHE_MAX_AGE_MS,
105
+ return runApiEffect(
106
+ fetchJsonEffect(input, init, schema, fallbackMessage, runtime),
262
107
  );
263
108
  }
264
109
 
265
- export function fetchQueryEnvelope(
266
- init?: RequestInit,
267
- {
268
- force = false,
269
- maxAgeMs = STATUS_CACHE_MAX_AGE_MS,
270
- }: ClientFetchCacheOptions = {},
271
- ) {
272
- const { requestInit, signal } = splitSignal(init);
273
- const request = loadClientCache(
274
- STATUS_CACHE_KEY,
275
- () => runApiEffect(fetchQueryEnvelopeEffect(requestInit)),
276
- { force, maxAgeMs },
277
- );
278
- return waitForSignal(request, signal);
110
+ export function fetchQueryEnvelope(init?: RequestInit) {
111
+ return runApiEffect(fetchQueryEnvelopeEffect(init));
279
112
  }
280
113
 
281
114
  export function fetchQueryEnvelopeEffect(init?: RequestInit) {
@@ -294,38 +127,6 @@ export function fetchQueryResponse(
294
127
  return runApiEffect(fetchQueryResponseEffect(input, init));
295
128
  }
296
129
 
297
- export function readCachedQueryResponse(input: RequestInfo | URL) {
298
- return readClientCache<QueryResponse>(
299
- queryCacheKey(input),
300
- QUERY_CACHE_MAX_AGE_MS,
301
- );
302
- }
303
-
304
- export function fetchCachedQueryResponse(
305
- input: RequestInfo | URL,
306
- init?: RequestInit,
307
- {
308
- force = false,
309
- maxAgeMs = QUERY_CACHE_MAX_AGE_MS,
310
- }: ClientFetchCacheOptions = {},
311
- ) {
312
- const { requestInit, signal } = splitSignal(init);
313
- const request = loadClientCache(
314
- queryCacheKey(input),
315
- () => runApiEffect(fetchQueryResponseEffect(input, requestInit)),
316
- { force, maxAgeMs },
317
- );
318
- return waitForSignal(request, signal);
319
- }
320
-
321
- export function invalidateCachedQueryResponse(input: RequestInfo | URL) {
322
- deleteClientCache(queryCacheKey(input));
323
- }
324
-
325
- export function invalidateCachedQueryResponses() {
326
- deleteClientCacheByPrefix(QUERY_CACHE_PREFIX);
327
- }
328
-
329
130
  export function fetchQueryResponseEffect(
330
131
  input: RequestInfo | URL,
331
132
  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;
@@ -0,0 +1,102 @@
1
+ export type ArchiveTweetRow = {
2
+ id: string;
3
+ kind: "home" | "like" | "bookmark";
4
+ authorProfileId: string;
5
+ text: string;
6
+ createdAt: string;
7
+ isReplied: number;
8
+ replyToId: string | null;
9
+ likeCount: number;
10
+ mediaCount: number;
11
+ bookmarked: number;
12
+ liked: number;
13
+ entitiesJson: string;
14
+ mediaJson: string;
15
+ quotedTweetId: string | null;
16
+ };
17
+
18
+ export type ArchiveCollectionRow = {
19
+ tweetId: string;
20
+ kind: "likes" | "bookmarks";
21
+ collectedAt: string | null;
22
+ source: string;
23
+ rawJson: string;
24
+ };
25
+
26
+ export type ArchiveProfileRow = {
27
+ id: string;
28
+ handle: string;
29
+ displayName: string;
30
+ bio: string;
31
+ followersCount: number;
32
+ followingCount: number;
33
+ publicMetricsJson: string;
34
+ avatarHue: number;
35
+ avatarUrl: string | null;
36
+ location: string | null;
37
+ url: string | null;
38
+ verifiedType: string | null;
39
+ entitiesJson: string;
40
+ rawJson: string;
41
+ createdAt: string;
42
+ };
43
+
44
+ export type ArchiveConversationRow = {
45
+ id: string;
46
+ title: string;
47
+ accountId: string;
48
+ participantProfileId: string;
49
+ lastMessageAt: string;
50
+ unreadCount: number;
51
+ needsReply: number;
52
+ };
53
+
54
+ export type ArchiveMessageRow = {
55
+ id: string;
56
+ conversationId: string;
57
+ senderProfileId: string;
58
+ text: string;
59
+ createdAt: string;
60
+ direction: "inbound" | "outbound";
61
+ mediaCount: number;
62
+ };
63
+
64
+ export type ArchiveFollowRow = {
65
+ profileId: string;
66
+ externalUserId: string;
67
+ };
68
+
69
+ export class ArchiveImportPlan {
70
+ readonly mentionDirectory = new Map<
71
+ string,
72
+ { handle?: string; displayName?: string }
73
+ >();
74
+ readonly tweets: ArchiveTweetRow[] = [];
75
+ readonly collections: ArchiveCollectionRow[] = [];
76
+ readonly profiles = new Map<string, ArchiveProfileRow>();
77
+ readonly conversations = new Map<string, ArchiveConversationRow>();
78
+ readonly dmMessages: ArchiveMessageRow[] = [];
79
+ readonly followers: ArchiveFollowRow[] = [];
80
+ readonly following: ArchiveFollowRow[] = [];
81
+ readonly followerIds = new Set<string>();
82
+ readonly followingIds = new Set<string>();
83
+
84
+ private readonly tweetsById = new Map<string, ArchiveTweetRow>();
85
+
86
+ addTweet(row: ArchiveTweetRow) {
87
+ const existing = this.tweetsById.get(row.id);
88
+ if (existing) {
89
+ existing.bookmarked = Math.max(existing.bookmarked, row.bookmarked);
90
+ existing.liked = Math.max(existing.liked, row.liked);
91
+ if (!existing.text && row.text) existing.text = row.text;
92
+ return existing;
93
+ }
94
+ this.tweets.push(row);
95
+ this.tweetsById.set(row.id, row);
96
+ return row;
97
+ }
98
+
99
+ getTweet(id: string) {
100
+ return this.tweetsById.get(id);
101
+ }
102
+ }