@usesocial/cli 0.7.1 → 0.8.0

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/dist/index.mjs CHANGED
@@ -129,855 +129,1014 @@ var CancelledError = class extends Error {
129
129
  }
130
130
  };
131
131
  //#endregion
132
- //#region ../../packages/sync-engine/src/migrations.generated.ts
133
- const MIGRATIONS = [
134
- {
135
- "tag": "0000_modern_stephen_strange",
136
- "sql": "CREATE TABLE `li_connections` (\n `id` text PRIMARY KEY NOT NULL,\n `created_at` integer,\n `object` text,\n `url` text,\n `user_id` text,\n `user_object` text,\n `user_type` text,\n `user_display_name` text,\n `user_public_identifier` text,\n `user_profile_url` text,\n `user_public_picture_url` text,\n `user_description` text,\n `raw` text NOT NULL,\n `synced_at` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE INDEX `li_connections_created_at_idx` ON `li_connections` (`created_at`);--> statement-breakpoint\nCREATE TABLE `li_conversations` (\n `id` text PRIMARY KEY NOT NULL,\n `created_at` integer,\n `last_message_timestamp` integer,\n `muted_until` integer,\n `creator_id` text,\n `headline` text,\n `image_url` text,\n `name` text,\n `object` text,\n `provider` text,\n `type` text,\n `user_id` text,\n `is_1to1` integer,\n `is_archived` integer,\n `is_channel` integer,\n `is_group` integer,\n `is_pinned` integer,\n `is_readonly` integer,\n `unread_count` integer,\n `user_object` text,\n `user_type` text,\n `user_display_name` text,\n `user_public_identifier` text,\n `user_profile_url` text,\n `user_public_picture_url` text,\n `user_description` text,\n `user_specifics_network_distance` text,\n `raw` text NOT NULL,\n `synced_at` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE INDEX `li_conversations_created_at_idx` ON `li_conversations` (`created_at`);--> statement-breakpoint\nCREATE INDEX `li_conversations_last_message_timestamp_idx` ON `li_conversations` (`last_message_timestamp`);--> statement-breakpoint\nCREATE TABLE `li_messages` (\n `id` text PRIMARY KEY NOT NULL,\n `timestamp` integer,\n `chat_id` text,\n `object` text,\n `provider` text,\n `text` text,\n `sender_id` text,\n `is_deleted` integer,\n `is_delivered` integer,\n `is_edited` integer,\n `is_event` integer,\n `is_hidden` integer,\n `is_mentionned` integer,\n `is_pinned` integer,\n `is_seen` integer,\n `is_sender` integer,\n `sender_object` text,\n `sender_type` text,\n `sender_display_name` text,\n `sender_public_identifier` text,\n `sender_profile_url` text,\n `sender_public_picture_url` text,\n `sender_description` text,\n `sender_specifics_network_distance` text,\n `raw` text NOT NULL,\n `synced_at` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE INDEX `li_messages_timestamp_idx` ON `li_messages` (`timestamp`);--> statement-breakpoint\nCREATE INDEX `li_messages_chat_id_idx` ON `li_messages` (`chat_id`);--> statement-breakpoint\nCREATE TABLE `li_posts` (\n `id` text PRIMARY KEY NOT NULL,\n `created_at` integer,\n `object` text,\n `share_url` text,\n `text` text,\n `url` text,\n `is_repost` integer,\n `user_reacted` text,\n `comments_counter` integer,\n `reposts_counter` integer,\n `analytics_impressions_counter` integer,\n `author_id` text,\n `author_object` text,\n `author_type` text,\n `author_display_name` text,\n `author_public_identifier` text,\n `author_profile_url` text,\n `author_public_picture_url` text,\n `author_description` text,\n `author_specifics_network_distance` text,\n `permissions_can_post_comments` integer,\n `permissions_can_react` integer,\n `permissions_can_share` integer,\n `poll_id` text,\n `poll_question` text,\n `poll_is_open` integer,\n `poll_total_votes_count` integer,\n `quoted_post_id` text,\n `quoted_post_object` text,\n `quoted_post_created_at` integer,\n `quoted_post_text` text,\n `quoted_post_share_url` text,\n `quoted_post_user_reacted` text,\n `quoted_post_comments_counter` integer,\n `quoted_post_reposts_counter` integer,\n `quoted_post_permissions_can_post_comments` integer,\n `quoted_post_permissions_can_react` integer,\n `quoted_post_permissions_can_share` integer,\n `quoted_post_author_id` text,\n `quoted_post_author_object` text,\n `quoted_post_author_type` text,\n `quoted_post_author_display_name` text,\n `quoted_post_author_public_identifier` text,\n `quoted_post_author_profile_url` text,\n `quoted_post_author_public_picture_url` text,\n `quoted_post_author_description` text,\n `quoted_post_author_specifics_network_distance` text,\n `reposted_by_id` text,\n `reposted_by_object` text,\n `reposted_by_type` text,\n `reposted_by_display_name` text,\n `reposted_by_public_identifier` text,\n `reposted_by_profile_url` text,\n `reposted_by_public_picture_url` text,\n `raw` text NOT NULL,\n `synced_at` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE INDEX `li_posts_created_at_idx` ON `li_posts` (`created_at`);--> statement-breakpoint\nCREATE TABLE `li_requests` (\n `id` text PRIMARY KEY NOT NULL,\n `created_at` integer,\n `object` text,\n `type` text,\n `user_id` text,\n `user_object` text,\n `user_type` text,\n `user_display_name` text,\n `user_public_identifier` text,\n `user_profile_url` text,\n `user_public_picture_url` text,\n `user_description` text,\n `raw` text NOT NULL,\n `synced_at` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE INDEX `li_requests_created_at_idx` ON `li_requests` (`created_at`);--> statement-breakpoint\nCREATE TABLE `sync_state` (\n `collection` text PRIMARY KEY NOT NULL,\n `cursor` text,\n `last_synced` integer,\n `object_count` integer\n);\n--> statement-breakpoint\nCREATE TABLE `x_bookmarks` (\n `id` text PRIMARY KEY NOT NULL,\n `created_at` integer,\n `author_id` text,\n `conversation_id` text,\n `in_reply_to_user_id` text,\n `lang` text,\n `reply_settings` text,\n `text` text,\n `url` text,\n `paid_partnership` integer,\n `possibly_sensitive` integer,\n `edit_controls_editable_until` integer,\n `edit_controls_edits_remaining` integer,\n `edit_controls_is_edit_eligible` integer,\n `like_count` integer,\n `retweet_count` integer,\n `reply_count` integer,\n `quote_count` integer,\n `bookmark_count` integer,\n `impression_count` integer,\n `article_cover_media` text,\n `article_plain_text` text,\n `article_preview_text` text,\n `article_title` text,\n `note_tweet_text` text,\n `raw` text NOT NULL,\n `synced_at` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE INDEX `x_bookmarks_created_at_idx` ON `x_bookmarks` (`created_at`);--> statement-breakpoint\nCREATE TABLE `x_followers` (\n `id` text PRIMARY KEY NOT NULL,\n `created_at` integer,\n `username` text,\n `name` text,\n `description` text,\n `location` text,\n `url` text,\n `is_identity_verified` integer,\n `protected` integer,\n `verified` integer,\n `verified_type` text,\n `subscription_type` text,\n `parody` integer,\n `most_recent_tweet_id` text,\n `pinned_tweet_id` text,\n `profile_banner_url` text,\n `profile_image_url` text,\n `affiliation_badge_url` text,\n `affiliation_description` text,\n `affiliation_url` text,\n `subscription_subscribes_to_you` integer,\n `followers_count` integer,\n `following_count` integer,\n `tweet_count` integer,\n `listed_count` integer,\n `like_count` integer,\n `media_count` integer,\n `raw` text NOT NULL,\n `synced_at` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE INDEX `x_followers_created_at_idx` ON `x_followers` (`created_at`);--> statement-breakpoint\nCREATE TABLE `x_following` (\n `id` text PRIMARY KEY NOT NULL,\n `created_at` integer,\n `username` text,\n `name` text,\n `description` text,\n `location` text,\n `url` text,\n `is_identity_verified` integer,\n `protected` integer,\n `verified` integer,\n `verified_type` text,\n `subscription_type` text,\n `parody` integer,\n `most_recent_tweet_id` text,\n `pinned_tweet_id` text,\n `profile_banner_url` text,\n `profile_image_url` text,\n `affiliation_badge_url` text,\n `affiliation_description` text,\n `affiliation_url` text,\n `subscription_subscribes_to_you` integer,\n `followers_count` integer,\n `following_count` integer,\n `tweet_count` integer,\n `listed_count` integer,\n `like_count` integer,\n `media_count` integer,\n `raw` text NOT NULL,\n `synced_at` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE INDEX `x_following_created_at_idx` ON `x_following` (`created_at`);--> statement-breakpoint\nCREATE TABLE `x_tweets` (\n `id` text PRIMARY KEY NOT NULL,\n `created_at` integer,\n `author_id` text,\n `conversation_id` text,\n `in_reply_to_user_id` text,\n `lang` text,\n `reply_settings` text,\n `text` text,\n `url` text,\n `paid_partnership` integer,\n `possibly_sensitive` integer,\n `edit_controls_editable_until` integer,\n `edit_controls_edits_remaining` integer,\n `edit_controls_is_edit_eligible` integer,\n `like_count` integer,\n `retweet_count` integer,\n `reply_count` integer,\n `quote_count` integer,\n `bookmark_count` integer,\n `impression_count` integer,\n `raw` text NOT NULL,\n `synced_at` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE INDEX `x_tweets_created_at_idx` ON `x_tweets` (`created_at`);"
137
- },
138
- {
139
- "tag": "0001_far_beast",
140
- "sql": "CREATE TABLE `li_profiles` (\n `provider_id` text PRIMARY KEY NOT NULL,\n `handle` text,\n `display_name` text NOT NULL,\n `avatar_url` text,\n `headline` text,\n `raw` text NOT NULL,\n `synced_at` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE INDEX `li_profiles_handle_idx` ON `li_profiles` (`handle`);--> statement-breakpoint\nCREATE TABLE `x_messages` (\n `id` text PRIMARY KEY NOT NULL,\n `created_at` integer,\n `dm_conversation_id` text,\n `event_type` text,\n `sender_id` text,\n `text` text,\n `participant_ids` text,\n `attachments` text,\n `entities` text,\n `referenced_tweets` text,\n `raw` text NOT NULL,\n `synced_at` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE INDEX `x_messages_created_at_idx` ON `x_messages` (`created_at`);--> statement-breakpoint\nCREATE INDEX `x_messages_dm_conversation_id_idx` ON `x_messages` (`dm_conversation_id`);--> statement-breakpoint\nCREATE TABLE `x_profiles` (\n `provider_id` text PRIMARY KEY NOT NULL,\n `handle` text,\n `display_name` text NOT NULL,\n `avatar_url` text,\n `headline` text,\n `raw` text NOT NULL,\n `synced_at` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE INDEX `x_profiles_handle_idx` ON `x_profiles` (`handle`);"
141
- },
142
- {
143
- "tag": "0002_previous_spitfire",
144
- "sql": "ALTER TABLE `li_connections` ADD `user_headline` text;--> statement-breakpoint\nALTER TABLE `li_requests` ADD `user_headline` text;--> statement-breakpoint\nALTER TABLE `x_bookmarks` DROP COLUMN `edit_controls_editable_until`;--> statement-breakpoint\nALTER TABLE `x_bookmarks` DROP COLUMN `edit_controls_edits_remaining`;--> statement-breakpoint\nALTER TABLE `x_bookmarks` DROP COLUMN `edit_controls_is_edit_eligible`;--> statement-breakpoint\nALTER TABLE `x_tweets` DROP COLUMN `edit_controls_editable_until`;--> statement-breakpoint\nALTER TABLE `x_tweets` DROP COLUMN `edit_controls_edits_remaining`;--> statement-breakpoint\nALTER TABLE `x_tweets` DROP COLUMN `edit_controls_is_edit_eligible`;"
145
- },
146
- {
147
- "tag": "0003_wonderful_makkari",
148
- "sql": "ALTER TABLE `li_conversations` RENAME COLUMN \"headline\" TO \"user_headline\";--> statement-breakpoint\nCREATE INDEX `li_conversations_user_id_last_message_timestamp_idx` ON `li_conversations` (`user_id`,`last_message_timestamp`);--> statement-breakpoint\nCREATE INDEX `li_conversations_user_public_identifier_last_message_timestamp_idx` ON `li_conversations` (`user_public_identifier`,`last_message_timestamp`);--> statement-breakpoint\nCREATE INDEX `li_conversations_user_display_name_idx` ON `li_conversations` (`user_display_name`);--> statement-breakpoint\nCREATE INDEX `li_conversations_is_archived_last_message_timestamp_idx` ON `li_conversations` (`is_archived`,`last_message_timestamp`);--> statement-breakpoint\nDROP INDEX `li_messages_chat_id_idx`;--> statement-breakpoint\nCREATE INDEX `li_messages_chat_id_timestamp_idx` ON `li_messages` (`chat_id`,`timestamp`);--> statement-breakpoint\nCREATE INDEX `li_messages_sender_id_timestamp_idx` ON `li_messages` (`sender_id`,`timestamp`);--> statement-breakpoint\nDROP INDEX `x_messages_dm_conversation_id_idx`;--> statement-breakpoint\nCREATE INDEX `x_messages_dm_conversation_id_created_at_idx` ON `x_messages` (`dm_conversation_id`,`created_at`);--> statement-breakpoint\nCREATE INDEX `x_messages_sender_id_created_at_idx` ON `x_messages` (`sender_id`,`created_at`);--> statement-breakpoint\nCREATE INDEX `li_connections_synced_at_idx` ON `li_connections` (`synced_at`);--> statement-breakpoint\nCREATE INDEX `li_connections_user_id_idx` ON `li_connections` (`user_id`);--> statement-breakpoint\nCREATE INDEX `li_connections_user_public_identifier_idx` ON `li_connections` (`user_public_identifier`);--> statement-breakpoint\nCREATE INDEX `li_connections_user_display_name_idx` ON `li_connections` (`user_display_name`);--> statement-breakpoint\nCREATE INDEX `li_posts_author_id_created_at_idx` ON `li_posts` (`author_id`,`created_at`);--> statement-breakpoint\nCREATE INDEX `li_posts_author_public_identifier_created_at_idx` ON `li_posts` (`author_public_identifier`,`created_at`);--> statement-breakpoint\nCREATE INDEX `li_requests_type_synced_at_idx` ON `li_requests` (`type`,`synced_at`);--> statement-breakpoint\nCREATE INDEX `li_requests_user_id_idx` ON `li_requests` (`user_id`);--> statement-breakpoint\nCREATE INDEX `li_requests_user_public_identifier_idx` ON `li_requests` (`user_public_identifier`);--> statement-breakpoint\nCREATE INDEX `li_requests_user_display_name_idx` ON `li_requests` (`user_display_name`);--> statement-breakpoint\nCREATE INDEX `x_bookmarks_author_id_created_at_idx` ON `x_bookmarks` (`author_id`,`created_at`);--> statement-breakpoint\nCREATE INDEX `x_bookmarks_conversation_id_created_at_idx` ON `x_bookmarks` (`conversation_id`,`created_at`);--> statement-breakpoint\nCREATE INDEX `x_followers_synced_at_idx` ON `x_followers` (`synced_at`);--> statement-breakpoint\nCREATE INDEX `x_followers_username_idx` ON `x_followers` (`username`);--> statement-breakpoint\nCREATE INDEX `x_followers_verified_idx` ON `x_followers` (`verified`);--> statement-breakpoint\nCREATE INDEX `x_followers_verified_type_idx` ON `x_followers` (`verified_type`);--> statement-breakpoint\nCREATE INDEX `x_following_synced_at_idx` ON `x_following` (`synced_at`);--> statement-breakpoint\nCREATE INDEX `x_following_username_idx` ON `x_following` (`username`);--> statement-breakpoint\nCREATE INDEX `x_following_verified_idx` ON `x_following` (`verified`);--> statement-breakpoint\nCREATE INDEX `x_following_verified_type_idx` ON `x_following` (`verified_type`);--> statement-breakpoint\nCREATE INDEX `x_tweets_author_id_created_at_idx` ON `x_tweets` (`author_id`,`created_at`);--> statement-breakpoint\nCREATE INDEX `x_tweets_conversation_id_created_at_idx` ON `x_tweets` (`conversation_id`,`created_at`);"
149
- },
150
- {
151
- "tag": "0004_quiet_prism",
152
- "sql": "ALTER TABLE `x_messages` RENAME TO `x_raw_messages`;--> statement-breakpoint\nCREATE VIEW `x_messages` AS\nSELECT m.*, p.handle AS sender_handle, p.display_name AS sender_name,\n p.avatar_url AS sender_avatar_url, p.headline AS sender_headline\nFROM `x_raw_messages` m LEFT JOIN `x_profiles` p ON m.sender_id = p.provider_id;\n"
153
- }
154
- ];
155
- //#endregion
156
- //#region ../../packages/sync-engine/src/migrate.ts
157
- const readUserVersion = (db) => {
158
- const version = db.prepare("PRAGMA user_version").get()?.user_version;
159
- return typeof version === "number" ? version : Number(version ?? 0);
160
- };
161
- const setUserVersion = (db, version) => {
162
- db.exec(`PRAGMA user_version = ${version}`);
163
- };
164
- const migrate = (db) => {
165
- const currentVersion = readUserVersion(db);
166
- if (currentVersion >= MIGRATIONS.length) return {
167
- fromVersion: currentVersion,
168
- toVersion: currentVersion,
169
- applied: 0
170
- };
171
- for (let index = currentVersion; index < MIGRATIONS.length; index += 1) {
172
- const migration = MIGRATIONS[index];
173
- if (!migration) throw new Error(`Missing migration at index ${index}`);
174
- db.transaction(() => {
175
- db.exec(migration.sql);
176
- setUserVersion(db, index + 1);
177
- });
178
- }
179
- return {
180
- fromVersion: currentVersion,
181
- toVersion: MIGRATIONS.length,
182
- applied: MIGRATIONS.length - currentVersion
183
- };
184
- };
185
- const assertAtLatest = (db) => {
186
- const currentVersion = readUserVersion(db);
187
- if (currentVersion < MIGRATIONS.length) throw new Error(`Local SQLite cache schema is at version ${currentVersion}; expected ${MIGRATIONS.length}`);
132
+ //#region ../../packages/sync-engine/src/collections/linkedin.ts
133
+ const linkedinCursorPagination = {
134
+ style: "cursor",
135
+ limitParam: "limit",
136
+ cursorParam: "cursor",
137
+ cursorPath: "next_cursor",
138
+ totalCountPath: "total_count",
139
+ itemsPath: "data"
188
140
  };
189
- //#endregion
190
- //#region ../../packages/sync-engine/src/sqlite.ts
191
- const loadModule = createRequire(import.meta.url);
192
- const isBunRuntime = () => typeof Bun !== "undefined";
193
- const normalizeValue = (value) => {
194
- if (value === null || value === void 0) return null;
195
- if (typeof value === "boolean") return value ? 1 : 0;
196
- return value;
141
+ const linkedinRelationsCursorPagination = {
142
+ style: "cursor",
143
+ limitParam: "limit",
144
+ cursorParam: "cursor",
145
+ cursorPath: "next_cursor",
146
+ itemsPath: "data"
197
147
  };
198
- const normalizeParams$1 = (params) => {
199
- if (!params) return;
200
- return Object.fromEntries(Object.entries(params).map(([key, value]) => [key, normalizeValue(value)]));
148
+ const asRecord$1 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
149
+ const read$1 = (value) => value === null || value === void 0 ? null : value;
150
+ const readReaction = (value) => typeof value === "string" ? value : null;
151
+ const trimmed$1 = (value) => typeof value === "string" ? value.trim() || void 0 : void 0;
152
+ const userHeadline = (user) => {
153
+ const specifics = asRecord$1(user.specifics);
154
+ return trimmed$1(user.description) ?? trimmed$1(user.headline) ?? trimmed$1(specifics.description) ?? trimmed$1(specifics.headline) ?? trimmed$1(specifics.occupation) ?? null;
201
155
  };
202
- const invokeWithParams = (fn, params) => {
203
- const normalizedParams = normalizeParams$1(params);
204
- return normalizedParams ? fn(normalizedParams) : fn();
156
+ const isoToEpochMs$1 = (value) => {
157
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
158
+ if (typeof value !== "string") return null;
159
+ const epochMs = Date.parse(value);
160
+ return Number.isNaN(epochMs) ? null : epochMs;
205
161
  };
206
- const wrapStatement = (statement) => ({
207
- run: (params) => invokeWithParams(statement.run.bind(statement), params),
208
- get: (params) => invokeWithParams(statement.get.bind(statement), params),
209
- all: (params) => invokeWithParams(statement.all.bind(statement), params)
210
- });
211
- const openRuntimeDatabase = (path, opts) => {
212
- if (isBunRuntime()) {
213
- const { Database } = loadModule("bun:sqlite");
214
- return new Database(path, {
215
- readonly: opts?.readonly,
216
- strict: true
217
- });
218
- }
219
- const { DatabaseSync } = loadModule("node:sqlite");
220
- return opts?.readonly ? new DatabaseSync(path, { readOnly: true }) : new DatabaseSync(path);
162
+ const toBool$1 = (value) => {
163
+ if (typeof value !== "boolean") return null;
164
+ return value ? 1 : 0;
221
165
  };
222
- const openDatabase = (path, opts) => {
223
- const database = openRuntimeDatabase(path, opts);
224
- const exec = (sql) => {
225
- database.exec(sql);
226
- };
166
+ const liftConnection = (item) => {
167
+ const user = asRecord$1(item.user);
227
168
  return {
228
- exec,
229
- prepare: (sql) => wrapStatement(database.prepare(sql)),
230
- transaction: (fn) => {
231
- exec("BEGIN");
232
- try {
233
- fn();
234
- exec("COMMIT");
235
- } catch (error) {
236
- exec("ROLLBACK");
237
- throw error;
238
- }
239
- },
240
- close: () => {
241
- database.close();
242
- }
169
+ id: read$1(item.id),
170
+ created_at: isoToEpochMs$1(item.created_at),
171
+ object: read$1(item.object),
172
+ url: read$1(item.url),
173
+ user_id: read$1(user.id),
174
+ user_object: read$1(user.object),
175
+ user_type: read$1(user.type),
176
+ user_display_name: read$1(user.display_name),
177
+ user_public_identifier: read$1(user.public_identifier),
178
+ user_profile_url: read$1(user.profile_url),
179
+ user_public_picture_url: read$1(user.public_picture_url),
180
+ user_description: read$1(user.description),
181
+ user_headline: userHeadline(user),
182
+ raw: JSON.stringify(item)
243
183
  };
244
184
  };
245
- //#endregion
246
- //#region ../../packages/sync-engine/src/cache.ts
247
- var NeverSyncedError = class extends Error {
248
- name = "NeverSyncedError";
249
- tables;
250
- constructor(tables, message) {
251
- super(message);
252
- this.tables = tables;
253
- }
254
- };
255
- const cacheTables = new Set([
256
- "x_tweets",
257
- "x_followers",
258
- "x_following",
259
- "x_bookmarks",
260
- "x_profiles",
261
- "x_messages",
262
- "x_raw_messages",
263
- "li_conversations",
264
- "li_messages",
265
- "li_connections",
266
- "li_posts",
267
- "li_profiles",
268
- "li_requests",
269
- "sync_state"
270
- ]);
271
- const profileTables = new Set(["x_profiles", "li_profiles"]);
272
- const sharedTables = new Set(["sync_state"]);
273
- const platformTables = {
274
- linkedin: new Set([...cacheTables].filter((table) => table.startsWith("li_"))),
275
- x: new Set([...cacheTables].filter((table) => table.startsWith("x_")))
185
+ const liftConversation = (item) => {
186
+ const user = asRecord$1(item.user);
187
+ const userSpecifics = asRecord$1(user.specifics);
188
+ return {
189
+ id: read$1(item.id),
190
+ created_at: isoToEpochMs$1(item.created_at),
191
+ last_message_timestamp: isoToEpochMs$1(item.last_message_timestamp),
192
+ muted_until: isoToEpochMs$1(item.muted_until),
193
+ creator_id: read$1(item.creator_id),
194
+ user_headline: userHeadline(user),
195
+ image_url: read$1(item.image_url),
196
+ name: read$1(item.name),
197
+ object: read$1(item.object),
198
+ provider: read$1(item.provider),
199
+ type: read$1(item.type),
200
+ user_id: read$1(item.user_id ?? user.id),
201
+ is_1to1: toBool$1(item.is_1to1),
202
+ is_archived: toBool$1(item.is_archived),
203
+ is_channel: toBool$1(item.is_channel),
204
+ is_group: toBool$1(item.is_group),
205
+ is_pinned: toBool$1(item.is_pinned),
206
+ is_readonly: toBool$1(item.is_readonly),
207
+ unread_count: read$1(item.unread_count),
208
+ user_object: read$1(user.object),
209
+ user_type: read$1(user.type),
210
+ user_display_name: read$1(user.display_name),
211
+ user_public_identifier: read$1(user.public_identifier),
212
+ user_profile_url: read$1(user.profile_url),
213
+ user_public_picture_url: read$1(user.public_picture_url),
214
+ user_description: read$1(user.description),
215
+ user_specifics_network_distance: read$1(userSpecifics.network_distance),
216
+ raw: JSON.stringify(item)
217
+ };
276
218
  };
277
- const sanitizeAccountId = (accountId) => accountId.replaceAll(/[^A-Za-z0-9._-]/g, "_");
278
- const quoteIdentifier = (identifier) => `\`${identifier.replaceAll("`", "``")}\``;
279
- const resolveCachePath = (path) => path === ":memory:" ? path : resolve(path);
280
- const stringOrNull = (value) => typeof value === "string" ? value : null;
281
- const numberOrNull = (value) => typeof value === "number" ? value : null;
282
- const assertKnownTable = (table) => {
283
- if (!cacheTables.has(table)) throw new Error(`Unknown cache table: ${table}`);
219
+ const liftMessage = (item) => {
220
+ const sender = asRecord$1(item.sender);
221
+ const senderSpecifics = asRecord$1(sender.specifics);
222
+ return {
223
+ id: read$1(item.id),
224
+ timestamp: isoToEpochMs$1(item.timestamp),
225
+ chat_id: read$1(item.chat_id ?? null),
226
+ object: read$1(item.object),
227
+ provider: read$1(item.provider),
228
+ text: read$1(item.text),
229
+ sender_id: read$1(item.sender_id),
230
+ is_deleted: toBool$1(item.is_deleted),
231
+ is_delivered: toBool$1(item.is_delivered),
232
+ is_edited: toBool$1(item.is_edited),
233
+ is_event: toBool$1(item.is_event),
234
+ is_hidden: toBool$1(item.is_hidden),
235
+ is_mentionned: toBool$1(item.is_mentionned),
236
+ is_pinned: toBool$1(item.is_pinned),
237
+ is_seen: toBool$1(item.is_seen),
238
+ is_sender: toBool$1(item.is_sender),
239
+ sender_object: read$1(sender.object),
240
+ sender_type: read$1(sender.type),
241
+ sender_display_name: read$1(sender.display_name),
242
+ sender_public_identifier: read$1(sender.public_identifier),
243
+ sender_profile_url: read$1(sender.profile_url),
244
+ sender_public_picture_url: read$1(sender.public_picture_url),
245
+ sender_description: read$1(sender.description),
246
+ sender_specifics_network_distance: read$1(senderSpecifics.network_distance),
247
+ raw: JSON.stringify(item)
248
+ };
284
249
  };
285
- const tablesForPlatform = (platform) => new Set([...platformTables[platform], ...sharedTables]);
286
- const knownTableTokens = (sql) => {
287
- const tokens = sql.match(/\b[A-Za-z_][A-Za-z0-9_]*\b/g) ?? [];
288
- const tables = [];
289
- const seen = /* @__PURE__ */ new Set();
290
- for (const token of tokens) {
291
- const table = token.toLowerCase();
292
- if (cacheTables.has(table) && !seen.has(table)) {
293
- seen.add(table);
294
- tables.push(table);
295
- }
296
- }
297
- return tables;
298
- };
299
- const assertPlatformQuery = (sql, platform) => {
300
- const allowedTables = tablesForPlatform(platform);
301
- const rejectedTables = new Set(knownTableTokens(sql).filter((table) => !allowedTables.has(table)));
302
- if (rejectedTables.size > 0) throw new Error(`Cache SQL for ${platform} cannot read ${[...rejectedTables].sort().join(", ")}.`);
250
+ const liftPost = (item) => {
251
+ const analytics = asRecord$1(item.analytics);
252
+ const author = asRecord$1(item.author);
253
+ const authorSpecifics = asRecord$1(author.specifics);
254
+ const permissions = asRecord$1(item.permissions);
255
+ const poll = asRecord$1(item.poll);
256
+ const quotedPost = asRecord$1(item.quoted_post);
257
+ const quotedPostPermissions = asRecord$1(quotedPost.permissions);
258
+ const quotedPostAuthor = asRecord$1(quotedPost.author);
259
+ const quotedPostAuthorSpecifics = asRecord$1(quotedPostAuthor.specifics);
260
+ const repostedBy = asRecord$1(item.reposted_by);
261
+ return {
262
+ id: read$1(item.id),
263
+ created_at: isoToEpochMs$1(item.created_at),
264
+ object: read$1(item.object),
265
+ share_url: read$1(item.share_url),
266
+ text: read$1(item.text),
267
+ url: read$1(item.url),
268
+ is_repost: toBool$1(item.is_repost),
269
+ user_reacted: readReaction(item.user_reacted),
270
+ comments_counter: read$1(item.comments_counter),
271
+ reposts_counter: read$1(item.reposts_counter),
272
+ analytics_impressions_counter: read$1(analytics.impressions_counter),
273
+ author_id: read$1(author.id),
274
+ author_object: read$1(author.object),
275
+ author_type: read$1(author.type),
276
+ author_display_name: read$1(author.display_name),
277
+ author_public_identifier: read$1(author.public_identifier),
278
+ author_profile_url: read$1(author.profile_url),
279
+ author_public_picture_url: read$1(author.public_picture_url),
280
+ author_description: read$1(author.description),
281
+ author_specifics_network_distance: read$1(authorSpecifics.network_distance),
282
+ permissions_can_post_comments: toBool$1(permissions.can_post_comments),
283
+ permissions_can_react: toBool$1(permissions.can_react),
284
+ permissions_can_share: toBool$1(permissions.can_share),
285
+ poll_id: read$1(poll.id),
286
+ poll_question: read$1(poll.question),
287
+ poll_is_open: toBool$1(poll.is_open),
288
+ poll_total_votes_count: read$1(poll.total_votes_count),
289
+ quoted_post_id: read$1(quotedPost.id),
290
+ quoted_post_object: read$1(quotedPost.object),
291
+ quoted_post_created_at: isoToEpochMs$1(quotedPost.created_at),
292
+ quoted_post_text: read$1(quotedPost.text),
293
+ quoted_post_share_url: read$1(quotedPost.share_url),
294
+ quoted_post_user_reacted: readReaction(quotedPost.user_reacted),
295
+ quoted_post_comments_counter: read$1(quotedPost.comments_counter),
296
+ quoted_post_reposts_counter: read$1(quotedPost.reposts_counter),
297
+ quoted_post_permissions_can_post_comments: toBool$1(quotedPostPermissions.can_post_comments),
298
+ quoted_post_permissions_can_react: toBool$1(quotedPostPermissions.can_react),
299
+ quoted_post_permissions_can_share: toBool$1(quotedPostPermissions.can_share),
300
+ quoted_post_author_id: read$1(quotedPostAuthor.id),
301
+ quoted_post_author_object: read$1(quotedPostAuthor.object),
302
+ quoted_post_author_type: read$1(quotedPostAuthor.type),
303
+ quoted_post_author_display_name: read$1(quotedPostAuthor.display_name),
304
+ quoted_post_author_public_identifier: read$1(quotedPostAuthor.public_identifier),
305
+ quoted_post_author_profile_url: read$1(quotedPostAuthor.profile_url),
306
+ quoted_post_author_public_picture_url: read$1(quotedPostAuthor.public_picture_url),
307
+ quoted_post_author_description: read$1(quotedPostAuthor.description),
308
+ quoted_post_author_specifics_network_distance: read$1(quotedPostAuthorSpecifics.network_distance),
309
+ reposted_by_id: read$1(repostedBy.id),
310
+ reposted_by_object: read$1(repostedBy.object),
311
+ reposted_by_type: read$1(repostedBy.type),
312
+ reposted_by_display_name: read$1(repostedBy.display_name),
313
+ reposted_by_public_identifier: read$1(repostedBy.public_identifier),
314
+ reposted_by_profile_url: read$1(repostedBy.profile_url),
315
+ reposted_by_public_picture_url: read$1(repostedBy.public_picture_url),
316
+ raw: JSON.stringify(item)
317
+ };
303
318
  };
304
- const referencedTablesForRead = (sql, platform) => {
305
- const allowedTables = platformTables[platform];
306
- return knownTableTokens(sql).filter((table) => allowedTables.has(table));
319
+ const liftRequest = (item) => {
320
+ const user = asRecord$1(item.user);
321
+ return {
322
+ id: read$1(item.id),
323
+ created_at: isoToEpochMs$1(item.created_at),
324
+ object: read$1(item.object),
325
+ type: read$1(item.type ?? null),
326
+ user_id: read$1(user.id),
327
+ user_object: read$1(user.object),
328
+ user_type: read$1(user.type),
329
+ user_display_name: read$1(user.display_name),
330
+ user_public_identifier: read$1(user.public_identifier),
331
+ user_profile_url: read$1(user.profile_url),
332
+ user_public_picture_url: read$1(user.public_picture_url),
333
+ user_description: read$1(user.description),
334
+ user_headline: userHeadline(user),
335
+ raw: JSON.stringify(item)
336
+ };
307
337
  };
308
- const collectionForReadTable = (table) => table === "x_raw_messages" ? "x_messages" : table;
309
- const collectionShortName = (collection) => collection.replace(/^(x|li)_/, "");
310
- const tableFreshnessPolicies = {
311
- x_raw_messages: {
312
- kind: "collection",
313
- collection: "x_messages"
338
+ const LINKEDIN_COLLECTIONS = [
339
+ {
340
+ key: "li_conversations",
341
+ table: "li_conversations_raw",
342
+ platform: "linkedin",
343
+ requests: [{ path: "/chats" }],
344
+ pagination: linkedinCursorPagination,
345
+ pageSize: 20,
346
+ sinceField: "last_message_timestamp",
347
+ lift: liftConversation
314
348
  },
315
- li_conversations: {
316
- kind: "collection",
317
- collection: "li_conversations",
318
- commandCollection: "li_messages"
349
+ {
350
+ key: "li_messages",
351
+ table: "li_messages_raw",
352
+ platform: "linkedin",
353
+ requests: [{ path: "/chats/:chat_id/messages" }],
354
+ parentCollection: "li_conversations",
355
+ parentIdToken: ":chat_id",
356
+ pagination: linkedinCursorPagination,
357
+ pageSize: 250,
358
+ sinceField: "timestamp",
359
+ lift: liftMessage
319
360
  },
320
- li_profiles: {
321
- kind: "platform",
322
- platform: "linkedin"
361
+ {
362
+ key: "li_connections",
363
+ table: "li_connections_raw",
364
+ platform: "linkedin",
365
+ requests: [{ path: "/users/connections" }],
366
+ requiresFullInitialSync: true,
367
+ pagination: linkedinRelationsCursorPagination,
368
+ pageSize: 100,
369
+ sinceField: "created_at",
370
+ lift: liftConnection
323
371
  },
324
- x_profiles: {
325
- kind: "platform",
326
- platform: "x"
372
+ {
373
+ key: "li_posts",
374
+ table: "li_posts_raw",
375
+ platform: "linkedin",
376
+ requests: [{ path: "/users/:identifier/posts" }],
377
+ ownIdToken: ":identifier",
378
+ pagination: linkedinCursorPagination,
379
+ pageSize: 50,
380
+ sinceField: "created_at",
381
+ lift: liftPost
382
+ },
383
+ {
384
+ key: "li_requests",
385
+ table: "li_requests_raw",
386
+ platform: "linkedin",
387
+ requests: [{
388
+ path: "/users/me/relation-requests/received",
389
+ bakedQuery: { type: "received" }
390
+ }, {
391
+ path: "/users/me/relation-requests/sent",
392
+ bakedQuery: { type: "sent" }
393
+ }],
394
+ pagination: linkedinRelationsCursorPagination,
395
+ pageSize: 100,
396
+ lift: liftRequest
397
+ }
398
+ ];
399
+ const userTypeValues = ["individual", "organization"];
400
+ const LINKEDIN_CACHE_METADATA = {
401
+ joins: [
402
+ "li_*.user_id = li_profiles.provider_id",
403
+ "li_messages.conversation_id = li_conversations.id",
404
+ "li_messages.sender_id = li_profiles.provider_id",
405
+ "li_posts.author_id = li_profiles.provider_id"
406
+ ],
407
+ enums: {
408
+ "li_connections.user_type": userTypeValues,
409
+ "li_conversations.type": ["1to1", "group"],
410
+ "li_conversations.user_type": userTypeValues,
411
+ "li_messages.sender_type": userTypeValues,
412
+ "li_posts.author_type": userTypeValues,
413
+ "li_requests.type": ["received", "sent"],
414
+ "li_requests.user_type": userTypeValues
415
+ },
416
+ indexed: {
417
+ li_connections: [
418
+ "created_at",
419
+ "user_id",
420
+ "user_username",
421
+ "user_name"
422
+ ],
423
+ li_conversations: [
424
+ "created_at",
425
+ "last_message_at",
426
+ "user_id",
427
+ "user_username",
428
+ "user_name",
429
+ "is_archived"
430
+ ],
431
+ li_messages: [
432
+ "created_at",
433
+ "conversation_id",
434
+ "sender_id"
435
+ ],
436
+ li_posts: [
437
+ "created_at",
438
+ "author_id",
439
+ "author_username"
440
+ ],
441
+ li_profiles: ["username"],
442
+ li_requests: [
443
+ "created_at",
444
+ "type",
445
+ "user_id",
446
+ "user_username",
447
+ "user_name"
448
+ ]
327
449
  }
328
450
  };
329
- const freshnessPolicyForTable = (table) => tableFreshnessPolicies[table] ?? {
330
- kind: "collection",
331
- collection: collectionForReadTable(table)
451
+ //#endregion
452
+ //#region ../../packages/sync-engine/src/collections/x.ts
453
+ const xPagination = {
454
+ style: "cursor",
455
+ limitParam: "max_results",
456
+ cursorParam: "pagination_token",
457
+ cursorPath: "meta.next_token",
458
+ itemsPath: "data"
332
459
  };
333
- const platformCollectionPrefix = (platform) => platform === "linkedin" ? "li_" : "x_";
334
- const syncCommandForPolicy = (platform, policy) => {
335
- if (policy.kind === "platform") return `social ${policy.platform} sync`;
336
- return `social ${platform} sync ${collectionShortName(policy.commandCollection ?? policy.collection)}`;
460
+ const defaultDMEventFields = "attachments,created_at,dm_conversation_id,entities,event_type,id,participant_ids,referenced_tweets,sender_id,text";
461
+ const defaultDMExpansions = "attachments.media_keys,participant_ids,referenced_tweets.id,sender_id";
462
+ const defaultDMUserFields = "affiliation,connection_status,created_at,description,entities,id,location,most_recent_tweet_id,name,pinned_tweet_id,profile_banner_url,profile_image_url,protected,public_metrics,receives_your_dm,subscription_type,url,username,verified,verified_type,withheld";
463
+ const asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
464
+ const read = (value) => value === null || value === void 0 ? null : value;
465
+ const isoToEpochMs = (value) => {
466
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
467
+ if (typeof value !== "string") return null;
468
+ const epochMs = Date.parse(value);
469
+ return Number.isNaN(epochMs) ? null : epochMs;
337
470
  };
338
- const missingNameForPolicy = (table, policy) => policy.kind === "platform" ? table : policy.collection;
339
- const formatList = (items) => {
340
- if (items.length <= 1) return items[0] ?? "";
341
- if (items.length === 2) return `${items[0]} and ${items[1]}`;
342
- return `${items.slice(0, -1).join(", ")}, and ${items.at(-1)}`;
471
+ const toBool = (value) => {
472
+ if (typeof value !== "boolean") return null;
473
+ return value ? 1 : 0;
343
474
  };
344
- const collectionLastSynced = (db, collection) => {
345
- return numberOrNull(db.prepare("SELECT last_synced FROM sync_state WHERE collection = :collection").get({ collection })?.last_synced);
475
+ const stringifyJSON$1 = (value) => value === null || value === void 0 ? null : JSON.stringify(value);
476
+ const liftTweetColumns = (item) => {
477
+ const publicMetrics = asRecord(item.public_metrics);
478
+ return {
479
+ id: read(item.id),
480
+ created_at: isoToEpochMs(item.created_at),
481
+ author_id: read(item.author_id),
482
+ conversation_id: read(item.conversation_id),
483
+ in_reply_to_user_id: read(item.in_reply_to_user_id),
484
+ lang: read(item.lang),
485
+ reply_settings: read(item.reply_settings),
486
+ text: read(item.text),
487
+ url: read(item.url),
488
+ paid_partnership: toBool(item.paid_partnership),
489
+ possibly_sensitive: toBool(item.possibly_sensitive),
490
+ like_count: read(publicMetrics.like_count),
491
+ retweet_count: read(publicMetrics.retweet_count),
492
+ reply_count: read(publicMetrics.reply_count),
493
+ quote_count: read(publicMetrics.quote_count),
494
+ bookmark_count: read(publicMetrics.bookmark_count),
495
+ impression_count: read(publicMetrics.impression_count)
496
+ };
346
497
  };
347
- const platformLastSynced = (db, platform) => {
348
- return numberOrNull(db.prepare("SELECT MAX(last_synced) AS last_synced FROM sync_state WHERE collection GLOB :glob").get({ glob: `${platformCollectionPrefix(platform)}*` })?.last_synced);
498
+ const liftTweet = (item) => ({
499
+ ...liftTweetColumns(item),
500
+ raw: JSON.stringify(item)
501
+ });
502
+ const liftBookmark = (item) => {
503
+ const article = asRecord(item.article);
504
+ const noteTweet = asRecord(item.note_tweet);
505
+ return {
506
+ ...liftTweetColumns(item),
507
+ article_cover_media: read(article.cover_media),
508
+ article_plain_text: read(article.plain_text),
509
+ article_preview_text: read(article.preview_text),
510
+ article_title: read(article.title),
511
+ note_tweet_text: read(noteTweet.text),
512
+ raw: JSON.stringify(item)
513
+ };
349
514
  };
350
- const lastSyncedForPolicy = (db, policy) => policy.kind === "platform" ? platformLastSynced(db, policy.platform) : collectionLastSynced(db, policy.collection);
351
- const missingFreshnessForTables = (tables) => {
352
- const missing = [];
353
- const seen = /* @__PURE__ */ new Set();
354
- for (const table of tables) {
355
- if (table.lastSynced != null) continue;
356
- const key = `${table.missingName}\0${table.command}`;
357
- if (!seen.has(key)) {
358
- seen.add(key);
359
- missing.push({
360
- name: table.missingName,
361
- command: table.command
362
- });
363
- }
515
+ const liftDMEvent = (item) => ({
516
+ id: read(item.id),
517
+ created_at: isoToEpochMs(item.created_at),
518
+ dm_conversation_id: read(item.dm_conversation_id),
519
+ event_type: read(item.event_type),
520
+ sender_id: read(item.sender_id),
521
+ text: read(item.text),
522
+ participant_ids: stringifyJSON$1(item.participant_ids),
523
+ attachments: stringifyJSON$1(item.attachments),
524
+ entities: stringifyJSON$1(item.entities),
525
+ referenced_tweets: stringifyJSON$1(item.referenced_tweets),
526
+ raw: JSON.stringify(item)
527
+ });
528
+ const liftXUser = (item) => {
529
+ const affiliation = asRecord(item.affiliation);
530
+ const publicMetrics = asRecord(item.public_metrics);
531
+ const subscription = asRecord(item.subscription);
532
+ return {
533
+ id: read(item.id),
534
+ created_at: isoToEpochMs(item.created_at),
535
+ username: read(item.username),
536
+ name: read(item.name),
537
+ description: read(item.description),
538
+ location: read(item.location),
539
+ url: read(item.url),
540
+ is_identity_verified: toBool(item.is_identity_verified),
541
+ protected: toBool(item.protected),
542
+ verified: toBool(item.verified),
543
+ verified_type: read(item.verified_type),
544
+ subscription_type: read(item.subscription_type),
545
+ parody: toBool(item.parody),
546
+ most_recent_tweet_id: read(item.most_recent_tweet_id),
547
+ pinned_tweet_id: read(item.pinned_tweet_id),
548
+ profile_banner_url: read(item.profile_banner_url),
549
+ profile_image_url: read(item.profile_image_url),
550
+ affiliation_badge_url: read(affiliation.badge_url),
551
+ affiliation_description: read(affiliation.description),
552
+ affiliation_url: read(affiliation.url),
553
+ subscription_subscribes_to_you: toBool(subscription.subscribes_to_you),
554
+ followers_count: read(publicMetrics.followers_count),
555
+ following_count: read(publicMetrics.following_count),
556
+ tweet_count: read(publicMetrics.tweet_count),
557
+ listed_count: read(publicMetrics.listed_count),
558
+ like_count: read(publicMetrics.like_count),
559
+ media_count: read(publicMetrics.media_count),
560
+ raw: JSON.stringify(item)
561
+ };
562
+ };
563
+ const X_COLLECTIONS = [
564
+ {
565
+ key: "x_tweets",
566
+ table: "x_tweets_raw",
567
+ platform: "x",
568
+ requests: [{ path: "/users/:id/tweets" }],
569
+ ownIdToken: ":id",
570
+ pagination: xPagination,
571
+ pageSize: 100,
572
+ sinceField: "created_at",
573
+ lift: liftTweet
574
+ },
575
+ {
576
+ key: "x_followers",
577
+ table: "x_followers_raw",
578
+ platform: "x",
579
+ requests: [{ path: "/users/:id/followers" }],
580
+ ownIdToken: ":id",
581
+ requiresFullInitialSync: true,
582
+ pagination: xPagination,
583
+ pageSize: 1e3,
584
+ lift: liftXUser
585
+ },
586
+ {
587
+ key: "x_following",
588
+ table: "x_following_raw",
589
+ platform: "x",
590
+ requests: [{ path: "/users/:id/following" }],
591
+ ownIdToken: ":id",
592
+ requiresFullInitialSync: true,
593
+ pagination: xPagination,
594
+ pageSize: 1e3,
595
+ lift: liftXUser
596
+ },
597
+ {
598
+ key: "x_bookmarks",
599
+ table: "x_bookmarks_raw",
600
+ platform: "x",
601
+ requests: [{ path: "/users/:id/bookmarks" }],
602
+ ownIdToken: ":id",
603
+ pagination: xPagination,
604
+ pageSize: 100,
605
+ lift: liftBookmark
606
+ },
607
+ {
608
+ key: "x_messages",
609
+ table: "x_messages_raw",
610
+ platform: "x",
611
+ requests: [{
612
+ path: "/dm_events",
613
+ bakedQuery: {
614
+ "dm_event.fields": defaultDMEventFields,
615
+ expansions: defaultDMExpansions,
616
+ "user.fields": defaultDMUserFields
617
+ }
618
+ }],
619
+ pagination: xPagination,
620
+ pageSize: 100,
621
+ sinceField: "created_at",
622
+ lift: liftDMEvent
623
+ }
624
+ ];
625
+ const X_CACHE_METADATA = {
626
+ joins: ["x_messages.sender_id = x_profiles.provider_id", "x_followers.id = x_profiles.provider_id"],
627
+ enums: {
628
+ "x_followers.verified_type": [
629
+ "blue",
630
+ "business",
631
+ "government",
632
+ "none"
633
+ ],
634
+ "x_following.verified_type": [
635
+ "blue",
636
+ "business",
637
+ "government",
638
+ "none"
639
+ ]
640
+ },
641
+ indexed: {
642
+ x_bookmarks: [
643
+ "created_at",
644
+ "author_id",
645
+ "conversation_id"
646
+ ],
647
+ x_followers: [
648
+ "created_at",
649
+ "username",
650
+ "verified",
651
+ "verified_type"
652
+ ],
653
+ x_following: [
654
+ "created_at",
655
+ "username",
656
+ "verified",
657
+ "verified_type"
658
+ ],
659
+ x_messages: [
660
+ "created_at",
661
+ "conversation_id",
662
+ "sender_id"
663
+ ],
664
+ x_profiles: ["username"],
665
+ x_tweets: [
666
+ "created_at",
667
+ "author_id",
668
+ "conversation_id"
669
+ ]
364
670
  }
365
- return missing;
366
671
  };
367
- const neverSyncedMessage = (missing) => {
368
- const names = missing.map((item) => item.name);
369
- const commands = missing.map((item) => `\`${item.command}\``);
370
- return `No synced ${formatList(names)} yet — run ${formatList(commands)} first.`;
672
+ //#endregion
673
+ //#region ../../packages/sync-engine/src/collections/index.ts
674
+ const COLLECTIONS = [...X_COLLECTIONS, ...LINKEDIN_COLLECTIONS];
675
+ const CACHE_METADATA_BY_PLATFORM = {
676
+ linkedin: LINKEDIN_CACHE_METADATA,
677
+ x: X_CACHE_METADATA
371
678
  };
372
- const assertKnownProfileTable = (table) => {
373
- assertKnownTable(table);
374
- if (!profileTables.has(table)) throw new Error(`Unknown profile table: ${table}`);
679
+ const collectionByKey = (key) => COLLECTIONS.find((collection) => collection.key === key);
680
+ const collectionsFor = (platform) => COLLECTIONS.filter((collection) => collection.platform === platform);
681
+ const syncableCollectionsFor = (platform) => {
682
+ const parentKeys = new Set(COLLECTIONS.map((collection) => collection.parentCollection).filter(Boolean));
683
+ return collectionsFor(platform).filter((collection) => !parentKeys.has(collection.key));
375
684
  };
376
- const cachePath = (args) => join(homedir(), ".social", "cache", `${args.platform}-${sanitizeAccountId(args.accountId)}.db`);
377
- const openCacheAt = (path, opts) => {
378
- const dbPath = resolveCachePath(path);
379
- if (opts?.readonly) {
380
- const db = openDatabase(dbPath, { readonly: true });
381
- assertAtLatest(db);
382
- return createCache(dbPath, db);
383
- }
384
- if (dbPath !== ":memory:") mkdirSync(dirname(dbPath), { recursive: true });
385
- const db = openDatabase(dbPath);
386
- const result = migrate(db);
387
- if (result.applied > 0) opts?.onMigrate?.(result);
388
- return createCache(dbPath, db);
685
+ const physicalTableFor = (collection) => collection.table ?? collection.key;
686
+ const resetTablesFor = (collection) => {
687
+ if (!collection.parentCollection) return [collection];
688
+ const parent = collectionByKey(collection.parentCollection);
689
+ return parent ? [collection, parent] : [collection];
389
690
  };
390
- const openCache = (args) => openCacheAt(cachePath(args), {
391
- readonly: args.readonly,
392
- onMigrate: args.onMigrate
393
- });
394
- const createCache = (path, db) => ({
395
- upsert: (table, rows, syncedAt = Date.now()) => {
396
- if (rows.length === 0) return;
397
- assertKnownTable(table);
398
- const columns = [...Object.keys(rows[0] ?? {}).filter((column) => column !== "synced_at"), "synced_at"];
399
- const updateColumns = columns.filter((column) => column !== "id");
400
- const statement = db.prepare(`INSERT INTO ${quoteIdentifier(table)} (${columns.map(quoteIdentifier).join(", ")}) VALUES (${columns.map((column) => `:${column}`).join(", ")}) ON CONFLICT(id) DO UPDATE SET ${updateColumns.map((column) => `${quoteIdentifier(column)}=excluded.${quoteIdentifier(column)}`).join(", ")}`);
401
- db.transaction(() => {
402
- for (const row of rows) statement.run({
403
- ...row,
404
- synced_at: syncedAt
405
- });
406
- });
691
+ const resetPhysicalTablesFor = (collection) => resetTablesFor(collection).map(physicalTableFor);
692
+ //#endregion
693
+ //#region ../../packages/sync-engine/src/migrations.generated.ts
694
+ const MIGRATIONS = [
695
+ {
696
+ "tag": "0000_modern_stephen_strange",
697
+ "sql": "CREATE TABLE `li_connections` (\n `id` text PRIMARY KEY NOT NULL,\n `created_at` integer,\n `object` text,\n `url` text,\n `user_id` text,\n `user_object` text,\n `user_type` text,\n `user_display_name` text,\n `user_public_identifier` text,\n `user_profile_url` text,\n `user_public_picture_url` text,\n `user_description` text,\n `raw` text NOT NULL,\n `synced_at` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE INDEX `li_connections_created_at_idx` ON `li_connections` (`created_at`);--> statement-breakpoint\nCREATE TABLE `li_conversations` (\n `id` text PRIMARY KEY NOT NULL,\n `created_at` integer,\n `last_message_timestamp` integer,\n `muted_until` integer,\n `creator_id` text,\n `headline` text,\n `image_url` text,\n `name` text,\n `object` text,\n `provider` text,\n `type` text,\n `user_id` text,\n `is_1to1` integer,\n `is_archived` integer,\n `is_channel` integer,\n `is_group` integer,\n `is_pinned` integer,\n `is_readonly` integer,\n `unread_count` integer,\n `user_object` text,\n `user_type` text,\n `user_display_name` text,\n `user_public_identifier` text,\n `user_profile_url` text,\n `user_public_picture_url` text,\n `user_description` text,\n `user_specifics_network_distance` text,\n `raw` text NOT NULL,\n `synced_at` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE INDEX `li_conversations_created_at_idx` ON `li_conversations` (`created_at`);--> statement-breakpoint\nCREATE INDEX `li_conversations_last_message_timestamp_idx` ON `li_conversations` (`last_message_timestamp`);--> statement-breakpoint\nCREATE TABLE `li_messages` (\n `id` text PRIMARY KEY NOT NULL,\n `timestamp` integer,\n `chat_id` text,\n `object` text,\n `provider` text,\n `text` text,\n `sender_id` text,\n `is_deleted` integer,\n `is_delivered` integer,\n `is_edited` integer,\n `is_event` integer,\n `is_hidden` integer,\n `is_mentionned` integer,\n `is_pinned` integer,\n `is_seen` integer,\n `is_sender` integer,\n `sender_object` text,\n `sender_type` text,\n `sender_display_name` text,\n `sender_public_identifier` text,\n `sender_profile_url` text,\n `sender_public_picture_url` text,\n `sender_description` text,\n `sender_specifics_network_distance` text,\n `raw` text NOT NULL,\n `synced_at` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE INDEX `li_messages_timestamp_idx` ON `li_messages` (`timestamp`);--> statement-breakpoint\nCREATE INDEX `li_messages_chat_id_idx` ON `li_messages` (`chat_id`);--> statement-breakpoint\nCREATE TABLE `li_posts` (\n `id` text PRIMARY KEY NOT NULL,\n `created_at` integer,\n `object` text,\n `share_url` text,\n `text` text,\n `url` text,\n `is_repost` integer,\n `user_reacted` text,\n `comments_counter` integer,\n `reposts_counter` integer,\n `analytics_impressions_counter` integer,\n `author_id` text,\n `author_object` text,\n `author_type` text,\n `author_display_name` text,\n `author_public_identifier` text,\n `author_profile_url` text,\n `author_public_picture_url` text,\n `author_description` text,\n `author_specifics_network_distance` text,\n `permissions_can_post_comments` integer,\n `permissions_can_react` integer,\n `permissions_can_share` integer,\n `poll_id` text,\n `poll_question` text,\n `poll_is_open` integer,\n `poll_total_votes_count` integer,\n `quoted_post_id` text,\n `quoted_post_object` text,\n `quoted_post_created_at` integer,\n `quoted_post_text` text,\n `quoted_post_share_url` text,\n `quoted_post_user_reacted` text,\n `quoted_post_comments_counter` integer,\n `quoted_post_reposts_counter` integer,\n `quoted_post_permissions_can_post_comments` integer,\n `quoted_post_permissions_can_react` integer,\n `quoted_post_permissions_can_share` integer,\n `quoted_post_author_id` text,\n `quoted_post_author_object` text,\n `quoted_post_author_type` text,\n `quoted_post_author_display_name` text,\n `quoted_post_author_public_identifier` text,\n `quoted_post_author_profile_url` text,\n `quoted_post_author_public_picture_url` text,\n `quoted_post_author_description` text,\n `quoted_post_author_specifics_network_distance` text,\n `reposted_by_id` text,\n `reposted_by_object` text,\n `reposted_by_type` text,\n `reposted_by_display_name` text,\n `reposted_by_public_identifier` text,\n `reposted_by_profile_url` text,\n `reposted_by_public_picture_url` text,\n `raw` text NOT NULL,\n `synced_at` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE INDEX `li_posts_created_at_idx` ON `li_posts` (`created_at`);--> statement-breakpoint\nCREATE TABLE `li_requests` (\n `id` text PRIMARY KEY NOT NULL,\n `created_at` integer,\n `object` text,\n `type` text,\n `user_id` text,\n `user_object` text,\n `user_type` text,\n `user_display_name` text,\n `user_public_identifier` text,\n `user_profile_url` text,\n `user_public_picture_url` text,\n `user_description` text,\n `raw` text NOT NULL,\n `synced_at` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE INDEX `li_requests_created_at_idx` ON `li_requests` (`created_at`);--> statement-breakpoint\nCREATE TABLE `sync_state` (\n `collection` text PRIMARY KEY NOT NULL,\n `cursor` text,\n `last_synced` integer,\n `object_count` integer\n);\n--> statement-breakpoint\nCREATE TABLE `x_bookmarks` (\n `id` text PRIMARY KEY NOT NULL,\n `created_at` integer,\n `author_id` text,\n `conversation_id` text,\n `in_reply_to_user_id` text,\n `lang` text,\n `reply_settings` text,\n `text` text,\n `url` text,\n `paid_partnership` integer,\n `possibly_sensitive` integer,\n `edit_controls_editable_until` integer,\n `edit_controls_edits_remaining` integer,\n `edit_controls_is_edit_eligible` integer,\n `like_count` integer,\n `retweet_count` integer,\n `reply_count` integer,\n `quote_count` integer,\n `bookmark_count` integer,\n `impression_count` integer,\n `article_cover_media` text,\n `article_plain_text` text,\n `article_preview_text` text,\n `article_title` text,\n `note_tweet_text` text,\n `raw` text NOT NULL,\n `synced_at` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE INDEX `x_bookmarks_created_at_idx` ON `x_bookmarks` (`created_at`);--> statement-breakpoint\nCREATE TABLE `x_followers` (\n `id` text PRIMARY KEY NOT NULL,\n `created_at` integer,\n `username` text,\n `name` text,\n `description` text,\n `location` text,\n `url` text,\n `is_identity_verified` integer,\n `protected` integer,\n `verified` integer,\n `verified_type` text,\n `subscription_type` text,\n `parody` integer,\n `most_recent_tweet_id` text,\n `pinned_tweet_id` text,\n `profile_banner_url` text,\n `profile_image_url` text,\n `affiliation_badge_url` text,\n `affiliation_description` text,\n `affiliation_url` text,\n `subscription_subscribes_to_you` integer,\n `followers_count` integer,\n `following_count` integer,\n `tweet_count` integer,\n `listed_count` integer,\n `like_count` integer,\n `media_count` integer,\n `raw` text NOT NULL,\n `synced_at` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE INDEX `x_followers_created_at_idx` ON `x_followers` (`created_at`);--> statement-breakpoint\nCREATE TABLE `x_following` (\n `id` text PRIMARY KEY NOT NULL,\n `created_at` integer,\n `username` text,\n `name` text,\n `description` text,\n `location` text,\n `url` text,\n `is_identity_verified` integer,\n `protected` integer,\n `verified` integer,\n `verified_type` text,\n `subscription_type` text,\n `parody` integer,\n `most_recent_tweet_id` text,\n `pinned_tweet_id` text,\n `profile_banner_url` text,\n `profile_image_url` text,\n `affiliation_badge_url` text,\n `affiliation_description` text,\n `affiliation_url` text,\n `subscription_subscribes_to_you` integer,\n `followers_count` integer,\n `following_count` integer,\n `tweet_count` integer,\n `listed_count` integer,\n `like_count` integer,\n `media_count` integer,\n `raw` text NOT NULL,\n `synced_at` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE INDEX `x_following_created_at_idx` ON `x_following` (`created_at`);--> statement-breakpoint\nCREATE TABLE `x_tweets` (\n `id` text PRIMARY KEY NOT NULL,\n `created_at` integer,\n `author_id` text,\n `conversation_id` text,\n `in_reply_to_user_id` text,\n `lang` text,\n `reply_settings` text,\n `text` text,\n `url` text,\n `paid_partnership` integer,\n `possibly_sensitive` integer,\n `edit_controls_editable_until` integer,\n `edit_controls_edits_remaining` integer,\n `edit_controls_is_edit_eligible` integer,\n `like_count` integer,\n `retweet_count` integer,\n `reply_count` integer,\n `quote_count` integer,\n `bookmark_count` integer,\n `impression_count` integer,\n `raw` text NOT NULL,\n `synced_at` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE INDEX `x_tweets_created_at_idx` ON `x_tweets` (`created_at`);"
407
698
  },
408
- upsertProfiles: (table, rows) => {
409
- if (rows.length === 0) return;
410
- assertKnownProfileTable(table);
411
- const rowsByProviderId = /* @__PURE__ */ new Map();
412
- for (const row of rows) rowsByProviderId.set(row.providerId, row);
413
- const columns = [
414
- "provider_id",
415
- "handle",
416
- "display_name",
417
- "avatar_url",
418
- "headline",
419
- "raw",
420
- "synced_at"
421
- ];
422
- const quotedTable = quoteIdentifier(table);
423
- const nullableColumns = [
424
- "handle",
425
- "avatar_url",
426
- "headline"
427
- ];
428
- const lastWriteColumns = [
429
- "display_name",
430
- "raw",
431
- "synced_at"
432
- ];
433
- const updateAssignments = [...nullableColumns.map((column) => `${quoteIdentifier(column)}=COALESCE(excluded.${quoteIdentifier(column)}, ${quotedTable}.${quoteIdentifier(column)})`), ...lastWriteColumns.map((column) => `${quoteIdentifier(column)}=excluded.${quoteIdentifier(column)}`)].join(", ");
434
- const statement = db.prepare(`INSERT INTO ${quotedTable} (${columns.map(quoteIdentifier).join(", ")}) VALUES (${columns.map((column) => `:${column}`).join(", ")}) ON CONFLICT(provider_id) DO UPDATE SET ${updateAssignments}`);
435
- const syncedAt = Date.now();
436
- db.transaction(() => {
437
- for (const row of rowsByProviderId.values()) statement.run({
438
- provider_id: row.providerId,
439
- handle: row.handle,
440
- display_name: row.displayName,
441
- avatar_url: row.avatarURL,
442
- headline: row.headline,
443
- raw: JSON.stringify(row.raw),
444
- synced_at: syncedAt
445
- });
446
- });
699
+ {
700
+ "tag": "0001_far_beast",
701
+ "sql": "CREATE TABLE `li_profiles` (\n `provider_id` text PRIMARY KEY NOT NULL,\n `handle` text,\n `display_name` text NOT NULL,\n `avatar_url` text,\n `headline` text,\n `raw` text NOT NULL,\n `synced_at` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE INDEX `li_profiles_handle_idx` ON `li_profiles` (`handle`);--> statement-breakpoint\nCREATE TABLE `x_messages` (\n `id` text PRIMARY KEY NOT NULL,\n `created_at` integer,\n `dm_conversation_id` text,\n `event_type` text,\n `sender_id` text,\n `text` text,\n `participant_ids` text,\n `attachments` text,\n `entities` text,\n `referenced_tweets` text,\n `raw` text NOT NULL,\n `synced_at` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE INDEX `x_messages_created_at_idx` ON `x_messages` (`created_at`);--> statement-breakpoint\nCREATE INDEX `x_messages_dm_conversation_id_idx` ON `x_messages` (`dm_conversation_id`);--> statement-breakpoint\nCREATE TABLE `x_profiles` (\n `provider_id` text PRIMARY KEY NOT NULL,\n `handle` text,\n `display_name` text NOT NULL,\n `avatar_url` text,\n `headline` text,\n `raw` text NOT NULL,\n `synced_at` integer NOT NULL\n);\n--> statement-breakpoint\nCREATE INDEX `x_profiles_handle_idx` ON `x_profiles` (`handle`);"
447
702
  },
448
- getCursor: (collection) => {
449
- const row = db.prepare("SELECT cursor, last_synced, object_count FROM sync_state WHERE collection = :collection").get({ collection });
450
- if (!row) return;
451
- return {
452
- cursor: stringOrNull(row.cursor),
453
- lastSynced: numberOrNull(row.last_synced),
454
- objectCount: numberOrNull(row.object_count)
455
- };
703
+ {
704
+ "tag": "0002_previous_spitfire",
705
+ "sql": "ALTER TABLE `li_connections` ADD `user_headline` text;--> statement-breakpoint\nALTER TABLE `li_requests` ADD `user_headline` text;--> statement-breakpoint\nALTER TABLE `x_bookmarks` DROP COLUMN `edit_controls_editable_until`;--> statement-breakpoint\nALTER TABLE `x_bookmarks` DROP COLUMN `edit_controls_edits_remaining`;--> statement-breakpoint\nALTER TABLE `x_bookmarks` DROP COLUMN `edit_controls_is_edit_eligible`;--> statement-breakpoint\nALTER TABLE `x_tweets` DROP COLUMN `edit_controls_editable_until`;--> statement-breakpoint\nALTER TABLE `x_tweets` DROP COLUMN `edit_controls_edits_remaining`;--> statement-breakpoint\nALTER TABLE `x_tweets` DROP COLUMN `edit_controls_is_edit_eligible`;"
456
706
  },
457
- setCursor: (collection, cursor, objectCount, lastSynced = Date.now()) => {
458
- db.prepare("INSERT INTO sync_state (collection, cursor, last_synced, object_count) VALUES (:collection, :cursor, :last_synced, :object_count) ON CONFLICT(collection) DO UPDATE SET cursor=excluded.cursor, last_synced=excluded.last_synced, object_count=excluded.object_count").run({
459
- collection,
460
- cursor,
461
- last_synced: lastSynced,
462
- object_count: objectCount
463
- });
707
+ {
708
+ "tag": "0003_wonderful_makkari",
709
+ "sql": "ALTER TABLE `li_conversations` RENAME COLUMN \"headline\" TO \"user_headline\";--> statement-breakpoint\nCREATE INDEX `li_conversations_user_id_last_message_timestamp_idx` ON `li_conversations` (`user_id`,`last_message_timestamp`);--> statement-breakpoint\nCREATE INDEX `li_conversations_user_public_identifier_last_message_timestamp_idx` ON `li_conversations` (`user_public_identifier`,`last_message_timestamp`);--> statement-breakpoint\nCREATE INDEX `li_conversations_user_display_name_idx` ON `li_conversations` (`user_display_name`);--> statement-breakpoint\nCREATE INDEX `li_conversations_is_archived_last_message_timestamp_idx` ON `li_conversations` (`is_archived`,`last_message_timestamp`);--> statement-breakpoint\nDROP INDEX `li_messages_chat_id_idx`;--> statement-breakpoint\nCREATE INDEX `li_messages_chat_id_timestamp_idx` ON `li_messages` (`chat_id`,`timestamp`);--> statement-breakpoint\nCREATE INDEX `li_messages_sender_id_timestamp_idx` ON `li_messages` (`sender_id`,`timestamp`);--> statement-breakpoint\nDROP INDEX `x_messages_dm_conversation_id_idx`;--> statement-breakpoint\nCREATE INDEX `x_messages_dm_conversation_id_created_at_idx` ON `x_messages` (`dm_conversation_id`,`created_at`);--> statement-breakpoint\nCREATE INDEX `x_messages_sender_id_created_at_idx` ON `x_messages` (`sender_id`,`created_at`);--> statement-breakpoint\nCREATE INDEX `li_connections_synced_at_idx` ON `li_connections` (`synced_at`);--> statement-breakpoint\nCREATE INDEX `li_connections_user_id_idx` ON `li_connections` (`user_id`);--> statement-breakpoint\nCREATE INDEX `li_connections_user_public_identifier_idx` ON `li_connections` (`user_public_identifier`);--> statement-breakpoint\nCREATE INDEX `li_connections_user_display_name_idx` ON `li_connections` (`user_display_name`);--> statement-breakpoint\nCREATE INDEX `li_posts_author_id_created_at_idx` ON `li_posts` (`author_id`,`created_at`);--> statement-breakpoint\nCREATE INDEX `li_posts_author_public_identifier_created_at_idx` ON `li_posts` (`author_public_identifier`,`created_at`);--> statement-breakpoint\nCREATE INDEX `li_requests_type_synced_at_idx` ON `li_requests` (`type`,`synced_at`);--> statement-breakpoint\nCREATE INDEX `li_requests_user_id_idx` ON `li_requests` (`user_id`);--> statement-breakpoint\nCREATE INDEX `li_requests_user_public_identifier_idx` ON `li_requests` (`user_public_identifier`);--> statement-breakpoint\nCREATE INDEX `li_requests_user_display_name_idx` ON `li_requests` (`user_display_name`);--> statement-breakpoint\nCREATE INDEX `x_bookmarks_author_id_created_at_idx` ON `x_bookmarks` (`author_id`,`created_at`);--> statement-breakpoint\nCREATE INDEX `x_bookmarks_conversation_id_created_at_idx` ON `x_bookmarks` (`conversation_id`,`created_at`);--> statement-breakpoint\nCREATE INDEX `x_followers_synced_at_idx` ON `x_followers` (`synced_at`);--> statement-breakpoint\nCREATE INDEX `x_followers_username_idx` ON `x_followers` (`username`);--> statement-breakpoint\nCREATE INDEX `x_followers_verified_idx` ON `x_followers` (`verified`);--> statement-breakpoint\nCREATE INDEX `x_followers_verified_type_idx` ON `x_followers` (`verified_type`);--> statement-breakpoint\nCREATE INDEX `x_following_synced_at_idx` ON `x_following` (`synced_at`);--> statement-breakpoint\nCREATE INDEX `x_following_username_idx` ON `x_following` (`username`);--> statement-breakpoint\nCREATE INDEX `x_following_verified_idx` ON `x_following` (`verified`);--> statement-breakpoint\nCREATE INDEX `x_following_verified_type_idx` ON `x_following` (`verified_type`);--> statement-breakpoint\nCREATE INDEX `x_tweets_author_id_created_at_idx` ON `x_tweets` (`author_id`,`created_at`);--> statement-breakpoint\nCREATE INDEX `x_tweets_conversation_id_created_at_idx` ON `x_tweets` (`conversation_id`,`created_at`);"
464
710
  },
465
- reset: ({ tables, collections }) => {
466
- for (const table of tables) assertKnownTable(table);
467
- const results = [];
468
- db.transaction(() => {
469
- for (const table of tables) {
470
- const result = db.prepare(`DELETE FROM ${quoteIdentifier(table)}`).run();
471
- results.push({
472
- table,
473
- rowsDeleted: result?.changes ?? 0
474
- });
475
- }
476
- const deleteState = db.prepare("DELETE FROM sync_state WHERE collection = :collection");
477
- for (const collection of collections) deleteState.run({ collection });
478
- });
479
- return results;
711
+ {
712
+ "tag": "0004_quiet_prism",
713
+ "sql": "ALTER TABLE `x_messages` RENAME TO `x_raw_messages`;--> statement-breakpoint\nCREATE VIEW `x_messages` AS\nSELECT m.*, p.handle AS sender_handle, p.display_name AS sender_name,\n p.avatar_url AS sender_avatar_url, p.headline AS sender_headline\nFROM `x_raw_messages` m LEFT JOIN `x_profiles` p ON m.sender_id = p.provider_id;\n"
480
714
  },
481
- query: (sql, opts) => {
482
- if (opts?.platform) assertPlatformQuery(sql, opts.platform);
483
- return db.prepare(sql).all();
715
+ {
716
+ "tag": "0005_raw_table_views",
717
+ "sql": "DROP VIEW IF EXISTS \"x_messages\";\nALTER TABLE \"x_tweets\" RENAME TO \"x_tweets_raw\";\nALTER TABLE \"x_bookmarks\" RENAME TO \"x_bookmarks_raw\";\nALTER TABLE \"x_followers\" RENAME TO \"x_followers_raw\";\nALTER TABLE \"x_following\" RENAME TO \"x_following_raw\";\nALTER TABLE \"x_profiles\" RENAME TO \"x_profiles_raw\";\nALTER TABLE \"x_raw_messages\" RENAME TO \"x_messages_raw\";\nALTER TABLE \"li_connections\" RENAME TO \"li_connections_raw\";\nALTER TABLE \"li_profiles\" RENAME TO \"li_profiles_raw\";\nALTER TABLE \"li_conversations\" RENAME TO \"li_conversations_raw\";\nALTER TABLE \"li_messages\" RENAME TO \"li_messages_raw\";\nALTER TABLE \"li_posts\" RENAME TO \"li_posts_raw\";\nALTER TABLE \"li_requests\" RENAME TO \"li_requests_raw\";\nCREATE VIEW \"x_tweets\" AS\nSELECT \"id\", \"created_at\", \"author_id\", \"conversation_id\", \"in_reply_to_user_id\", \"lang\",\n \"reply_settings\", \"text\", \"url\", \"paid_partnership\", \"possibly_sensitive\",\n \"like_count\", \"retweet_count\", \"reply_count\", \"quote_count\", \"bookmark_count\",\n \"impression_count\"\nFROM \"x_tweets_raw\";\nCREATE VIEW \"x_bookmarks\" AS\nSELECT \"id\", \"created_at\", \"author_id\", \"conversation_id\", \"in_reply_to_user_id\", \"lang\",\n \"reply_settings\", \"text\", \"url\", \"paid_partnership\", \"possibly_sensitive\",\n \"like_count\", \"retweet_count\", \"reply_count\", \"quote_count\", \"bookmark_count\",\n \"impression_count\", \"article_cover_media\", \"article_plain_text\",\n \"article_preview_text\", \"article_title\", \"note_tweet_text\"\nFROM \"x_bookmarks_raw\";\nCREATE VIEW \"x_followers\" AS\nSELECT \"id\", \"created_at\", \"username\", \"name\", \"description\", \"location\", \"url\",\n \"verified\", \"verified_type\", \"is_identity_verified\", \"protected\",\n \"subscription_type\", \"parody\", \"followers_count\", \"following_count\",\n \"tweet_count\", \"listed_count\", \"like_count\", \"media_count\", \"most_recent_tweet_id\",\n \"pinned_tweet_id\", \"profile_image_url\" AS \"avatar_url\", \"profile_banner_url\",\n \"affiliation_description\", \"affiliation_url\", \"affiliation_badge_url\",\n \"subscription_subscribes_to_you\"\nFROM \"x_followers_raw\";\nCREATE VIEW \"x_following\" AS\nSELECT \"id\", \"created_at\", \"username\", \"name\", \"description\", \"location\", \"url\",\n \"verified\", \"verified_type\", \"is_identity_verified\", \"protected\",\n \"subscription_type\", \"parody\", \"followers_count\", \"following_count\",\n \"tweet_count\", \"listed_count\", \"like_count\", \"media_count\", \"most_recent_tweet_id\",\n \"pinned_tweet_id\", \"profile_image_url\" AS \"avatar_url\", \"profile_banner_url\",\n \"affiliation_description\", \"affiliation_url\", \"affiliation_badge_url\",\n \"subscription_subscribes_to_you\"\nFROM \"x_following_raw\";\nCREATE VIEW \"x_messages\" AS\nSELECT m.\"id\", m.\"created_at\", m.\"dm_conversation_id\" AS \"conversation_id\",\n m.\"event_type\", m.\"sender_id\", m.\"text\", m.\"participant_ids\", m.\"attachments\",\n m.\"entities\", m.\"referenced_tweets\", p.\"handle\" AS \"sender_username\",\n p.\"display_name\" AS \"sender_name\", p.\"avatar_url\" AS \"sender_avatar_url\",\n p.\"headline\" AS \"sender_headline\"\nFROM \"x_messages_raw\" m\nLEFT JOIN \"x_profiles_raw\" p ON m.\"sender_id\" = p.\"provider_id\";\nCREATE VIEW \"x_profiles\" AS\nSELECT \"provider_id\", \"handle\" AS \"username\", \"display_name\" AS \"name\", \"avatar_url\",\n \"headline\", json_extract(\"raw\", '$.receives_your_dm') AS \"receives_your_dm\"\nFROM \"x_profiles_raw\";\nCREATE VIEW \"li_profiles\" AS\nSELECT \"provider_id\", \"handle\" AS \"username\", \"display_name\" AS \"name\", \"avatar_url\",\n \"headline\"\nFROM \"li_profiles_raw\";\nCREATE VIEW \"li_connections\" AS\nSELECT \"id\", \"created_at\", \"url\", \"user_id\", \"user_public_identifier\" AS \"user_username\",\n \"user_display_name\" AS \"user_name\", \"user_headline\", \"user_description\",\n \"user_profile_url\", \"user_public_picture_url\" AS \"user_avatar_url\", \"user_type\"\nFROM \"li_connections_raw\";\nCREATE VIEW \"li_requests\" AS\nSELECT \"id\", \"created_at\", \"type\", json_extract(\"raw\", '$.message') AS \"message\",\n \"user_id\", \"user_public_identifier\" AS \"user_username\",\n \"user_display_name\" AS \"user_name\", \"user_headline\", \"user_description\",\n \"user_profile_url\", \"user_public_picture_url\" AS \"user_avatar_url\", \"user_type\"\nFROM \"li_requests_raw\";\nCREATE VIEW \"li_conversations\" AS\nSELECT \"id\", \"created_at\", \"last_message_timestamp\" AS \"last_message_at\", \"name\",\n CASE\n WHEN \"is_1to1\" = 1 THEN '1to1'\n WHEN \"is_group\" = 1 THEN 'group'\n ELSE \"type\"\n END AS \"type\",\n \"unread_count\", \"is_archived\", \"is_pinned\", \"is_readonly\", \"is_channel\",\n \"muted_until\", \"image_url\", \"creator_id\", \"user_id\",\n \"user_public_identifier\" AS \"user_username\", \"user_display_name\" AS \"user_name\",\n \"user_headline\", \"user_description\", \"user_profile_url\",\n \"user_public_picture_url\" AS \"user_avatar_url\", \"user_type\",\n \"user_specifics_network_distance\" AS \"user_distance\"\nFROM \"li_conversations_raw\";\nCREATE VIEW \"li_messages\" AS\nSELECT \"id\", \"timestamp\" AS \"created_at\", \"chat_id\" AS \"conversation_id\", \"text\",\n \"sender_id\", \"is_deleted\", \"is_delivered\", \"is_edited\", \"is_event\", \"is_hidden\",\n \"is_mentionned\" AS \"is_mentioned\", \"is_pinned\", \"is_seen\", \"is_sender\",\n \"sender_public_identifier\" AS \"sender_username\",\n \"sender_display_name\" AS \"sender_name\", \"sender_description\" AS \"sender_headline\",\n \"sender_public_picture_url\" AS \"sender_avatar_url\", \"sender_profile_url\",\n \"sender_type\", \"sender_specifics_network_distance\" AS \"sender_distance\"\nFROM \"li_messages_raw\";\nCREATE VIEW \"li_posts\" AS\nSELECT \"id\", \"created_at\", \"text\", \"url\", \"share_url\", \"is_repost\", \"user_reacted\",\n \"comments_counter\", \"reposts_counter\",\n \"analytics_impressions_counter\" AS \"impressions_counter\", \"author_id\",\n \"author_public_identifier\" AS \"author_username\",\n \"author_display_name\" AS \"author_name\", \"author_description\", \"author_profile_url\",\n \"author_public_picture_url\" AS \"author_avatar_url\", \"author_type\",\n \"author_specifics_network_distance\" AS \"author_distance\", \"poll_id\", \"poll_question\",\n \"poll_is_open\", \"poll_total_votes_count\", \"quoted_post_id\", \"quoted_post_created_at\",\n \"quoted_post_text\", \"quoted_post_author_public_identifier\" AS \"quoted_post_author_username\",\n \"quoted_post_author_display_name\" AS \"quoted_post_author_name\", \"reposted_by_id\",\n \"reposted_by_public_identifier\" AS \"reposted_by_username\",\n \"reposted_by_display_name\" AS \"reposted_by_name\"\nFROM \"li_posts_raw\";\n"
484
718
  },
485
- read: (sql, opts) => {
486
- assertPlatformQuery(sql, opts.platform);
487
- const tables = referencedTablesForRead(sql, opts.platform).map((name) => {
488
- const policy = freshnessPolicyForTable(name);
489
- return {
490
- name,
491
- missingName: missingNameForPolicy(name, policy),
492
- command: syncCommandForPolicy(opts.platform, policy),
493
- lastSynced: lastSyncedForPolicy(db, policy)
494
- };
495
- });
496
- const missingFreshness = missingFreshnessForTables(tables);
497
- if (missingFreshness.length > 0) throw new NeverSyncedError(missingFreshness.map((item) => item.name), neverSyncedMessage(missingFreshness));
498
- return {
499
- rows: db.prepare(sql).all(),
500
- tables: tables.map(({ name, lastSynced }) => ({
501
- name,
502
- lastSynced
503
- }))
504
- };
505
- },
506
- describe: (opts) => {
507
- const allowedTables = opts?.platform ? tablesForPlatform(opts.platform) : void 0;
508
- const tables = db.prepare("SELECT name, type, sql FROM sqlite_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%' ORDER BY name").all().filter((table) => String(table.name) !== "x_raw_messages").filter((table) => allowedTables === void 0 || allowedTables.has(String(table.name)));
509
- const lastSyncedForTable = (name) => {
510
- if (name === "sync_state") return null;
511
- return lastSyncedForPolicy(db, freshnessPolicyForTable(name));
512
- };
513
- return {
514
- path,
515
- tables: tables.map((table) => {
516
- const name = String(table.name);
517
- const type = table.type === "view" ? "view" : "table";
518
- const rowCount = db.prepare(`SELECT count(*) AS rows FROM ${quoteIdentifier(name)}`).get();
519
- return {
520
- name,
521
- type,
522
- sql: String(table.sql),
523
- rows: Number(rowCount?.rows ?? 0),
524
- last_synced: lastSyncedForTable(name)
525
- };
526
- })
527
- };
719
+ {
720
+ "tag": "0006_username_profiles",
721
+ "sql": "DROP VIEW IF EXISTS \"x_messages\";\nDROP VIEW IF EXISTS \"x_profiles\";\nDROP VIEW IF EXISTS \"li_profiles\";\nALTER TABLE \"x_profiles_raw\" RENAME COLUMN \"handle\" TO \"username\";\nALTER TABLE \"li_profiles_raw\" RENAME COLUMN \"handle\" TO \"username\";\nDROP INDEX IF EXISTS \"x_profiles_handle_idx\";\nDROP INDEX IF EXISTS \"li_profiles_handle_idx\";\nCREATE INDEX \"x_profiles_username_idx\" ON \"x_profiles_raw\" (\"username\");\nCREATE INDEX \"li_profiles_username_idx\" ON \"li_profiles_raw\" (\"username\");\nCREATE VIEW \"x_messages\" AS\nSELECT m.\"id\", m.\"created_at\", m.\"dm_conversation_id\" AS \"conversation_id\",\n m.\"event_type\", m.\"sender_id\", m.\"text\", m.\"participant_ids\", m.\"attachments\",\n m.\"entities\", m.\"referenced_tweets\", p.\"username\" AS \"sender_username\",\n p.\"display_name\" AS \"sender_name\", p.\"avatar_url\" AS \"sender_avatar_url\",\n p.\"headline\" AS \"sender_headline\"\nFROM \"x_messages_raw\" m\nLEFT JOIN \"x_profiles_raw\" p ON m.\"sender_id\" = p.\"provider_id\";\nCREATE VIEW \"x_profiles\" AS\nSELECT \"provider_id\", \"username\", \"display_name\" AS \"name\", \"avatar_url\",\n \"headline\", json_extract(\"raw\", '$.receives_your_dm') AS \"receives_your_dm\"\nFROM \"x_profiles_raw\";\nCREATE VIEW \"li_profiles\" AS\nSELECT \"provider_id\", \"username\", \"display_name\" AS \"name\", \"avatar_url\", \"headline\"\nFROM \"li_profiles_raw\";\n"
528
722
  },
529
- close: () => {
530
- db.close();
723
+ {
724
+ "tag": "0007_connection_headline_coalesce",
725
+ "sql": "DROP VIEW IF EXISTS \"li_connections\";\nDROP VIEW IF EXISTS \"li_requests\";\nDROP VIEW IF EXISTS \"li_conversations\";\nCREATE VIEW \"li_connections\" AS\nSELECT \"id\", \"created_at\", \"url\", \"user_id\", \"user_public_identifier\" AS \"user_username\",\n \"user_display_name\" AS \"user_name\",\n COALESCE(\"user_headline\", \"user_description\") AS \"user_headline\", \"user_description\",\n \"user_profile_url\", \"user_public_picture_url\" AS \"user_avatar_url\", \"user_type\"\nFROM \"li_connections_raw\";\nCREATE VIEW \"li_requests\" AS\nSELECT \"id\", \"created_at\", \"type\", json_extract(\"raw\", '$.message') AS \"message\",\n \"user_id\", \"user_public_identifier\" AS \"user_username\",\n \"user_display_name\" AS \"user_name\",\n COALESCE(\"user_headline\", \"user_description\") AS \"user_headline\", \"user_description\",\n \"user_profile_url\", \"user_public_picture_url\" AS \"user_avatar_url\", \"user_type\"\nFROM \"li_requests_raw\";\nCREATE VIEW \"li_conversations\" AS\nSELECT \"id\", \"created_at\", \"last_message_timestamp\" AS \"last_message_at\", \"name\",\n CASE\n WHEN \"is_1to1\" = 1 THEN '1to1'\n WHEN \"is_group\" = 1 THEN 'group'\n ELSE \"type\"\n END AS \"type\",\n \"unread_count\", \"is_archived\", \"is_pinned\", \"is_readonly\", \"is_channel\",\n \"muted_until\", \"image_url\", \"creator_id\", \"user_id\",\n \"user_public_identifier\" AS \"user_username\", \"user_display_name\" AS \"user_name\",\n COALESCE(\"user_headline\", \"user_description\") AS \"user_headline\", \"user_description\",\n \"user_profile_url\", \"user_public_picture_url\" AS \"user_avatar_url\", \"user_type\",\n \"user_specifics_network_distance\" AS \"user_distance\"\nFROM \"li_conversations_raw\";\n"
531
726
  }
532
- });
727
+ ];
533
728
  //#endregion
534
- //#region ../../packages/sync-engine/src/collections/linkedin.ts
535
- const linkedinCursorPagination = {
536
- style: "cursor",
537
- limitParam: "limit",
538
- cursorParam: "cursor",
539
- cursorPath: "next_cursor",
540
- totalCountPath: "total_count",
541
- itemsPath: "data"
729
+ //#region ../../packages/sync-engine/src/migrate.ts
730
+ const readUserVersion = (db) => {
731
+ const version = db.prepare("PRAGMA user_version").get()?.user_version;
732
+ return typeof version === "number" ? version : Number(version ?? 0);
542
733
  };
543
- const linkedinRelationsCursorPagination = {
544
- style: "cursor",
545
- limitParam: "limit",
546
- cursorParam: "cursor",
547
- cursorPath: "next_cursor",
548
- itemsPath: "data"
734
+ const setUserVersion = (db, version) => {
735
+ db.exec(`PRAGMA user_version = ${version}`);
549
736
  };
550
- const asRecord$1 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
551
- const read$1 = (value) => value === null || value === void 0 ? null : value;
552
- const readReaction = (value) => typeof value === "string" ? value : null;
553
- const trimmed$1 = (value) => typeof value === "string" ? value.trim() || void 0 : void 0;
554
- const userHeadline = (user) => {
555
- const specifics = asRecord$1(user.specifics);
556
- return trimmed$1(user.description) ?? trimmed$1(user.headline) ?? trimmed$1(specifics.description) ?? trimmed$1(specifics.headline) ?? trimmed$1(specifics.occupation) ?? null;
737
+ const migrate = (db) => {
738
+ const currentVersion = readUserVersion(db);
739
+ if (currentVersion >= MIGRATIONS.length) return {
740
+ fromVersion: currentVersion,
741
+ toVersion: currentVersion,
742
+ applied: 0
743
+ };
744
+ for (let index = currentVersion; index < MIGRATIONS.length; index += 1) {
745
+ const migration = MIGRATIONS[index];
746
+ if (!migration) throw new Error(`Missing migration at index ${index}`);
747
+ db.transaction(() => {
748
+ db.exec(migration.sql);
749
+ setUserVersion(db, index + 1);
750
+ });
751
+ }
752
+ return {
753
+ fromVersion: currentVersion,
754
+ toVersion: MIGRATIONS.length,
755
+ applied: MIGRATIONS.length - currentVersion
756
+ };
557
757
  };
558
- const isoToEpochMs$1 = (value) => {
559
- if (typeof value === "number") return Number.isFinite(value) ? value : null;
560
- if (typeof value !== "string") return null;
561
- const epochMs = Date.parse(value);
562
- return Number.isNaN(epochMs) ? null : epochMs;
758
+ const assertAtLatest = (db) => {
759
+ const currentVersion = readUserVersion(db);
760
+ if (currentVersion < MIGRATIONS.length) throw new Error(`Local SQLite cache schema is at version ${currentVersion}; expected ${MIGRATIONS.length}`);
563
761
  };
564
- const toBool$1 = (value) => {
565
- if (typeof value !== "boolean") return null;
566
- return value ? 1 : 0;
762
+ //#endregion
763
+ //#region ../../packages/sync-engine/src/sqlite.ts
764
+ const loadModule = createRequire(import.meta.url);
765
+ const isBunRuntime = () => typeof Bun !== "undefined";
766
+ const normalizeValue = (value) => {
767
+ if (value === null || value === void 0) return null;
768
+ if (typeof value === "boolean") return value ? 1 : 0;
769
+ return value;
567
770
  };
568
- const liftConnection = (item) => {
569
- const user = asRecord$1(item.user);
570
- return {
571
- id: read$1(item.id),
572
- created_at: isoToEpochMs$1(item.created_at),
573
- object: read$1(item.object),
574
- url: read$1(item.url),
575
- user_id: read$1(user.id),
576
- user_object: read$1(user.object),
577
- user_type: read$1(user.type),
578
- user_display_name: read$1(user.display_name),
579
- user_public_identifier: read$1(user.public_identifier),
580
- user_profile_url: read$1(user.profile_url),
581
- user_public_picture_url: read$1(user.public_picture_url),
582
- user_description: read$1(user.description),
583
- user_headline: userHeadline(user),
584
- raw: JSON.stringify(item)
585
- };
771
+ const normalizeParams$1 = (params) => {
772
+ if (!params) return;
773
+ return Object.fromEntries(Object.entries(params).map(([key, value]) => [key, normalizeValue(value)]));
586
774
  };
587
- const liftConversation = (item) => {
588
- const user = asRecord$1(item.user);
589
- const userSpecifics = asRecord$1(user.specifics);
590
- return {
591
- id: read$1(item.id),
592
- created_at: isoToEpochMs$1(item.created_at),
593
- last_message_timestamp: isoToEpochMs$1(item.last_message_timestamp),
594
- muted_until: isoToEpochMs$1(item.muted_until),
595
- creator_id: read$1(item.creator_id),
596
- user_headline: userHeadline(user),
597
- image_url: read$1(item.image_url),
598
- name: read$1(item.name),
599
- object: read$1(item.object),
600
- provider: read$1(item.provider),
601
- type: read$1(item.type),
602
- user_id: read$1(item.user_id ?? user.id),
603
- is_1to1: toBool$1(item.is_1to1),
604
- is_archived: toBool$1(item.is_archived),
605
- is_channel: toBool$1(item.is_channel),
606
- is_group: toBool$1(item.is_group),
607
- is_pinned: toBool$1(item.is_pinned),
608
- is_readonly: toBool$1(item.is_readonly),
609
- unread_count: read$1(item.unread_count),
610
- user_object: read$1(user.object),
611
- user_type: read$1(user.type),
612
- user_display_name: read$1(user.display_name),
613
- user_public_identifier: read$1(user.public_identifier),
614
- user_profile_url: read$1(user.profile_url),
615
- user_public_picture_url: read$1(user.public_picture_url),
616
- user_description: read$1(user.description),
617
- user_specifics_network_distance: read$1(userSpecifics.network_distance),
618
- raw: JSON.stringify(item)
619
- };
775
+ const invokeWithParams = (fn, params) => {
776
+ const normalizedParams = normalizeParams$1(params);
777
+ return normalizedParams ? fn(normalizedParams) : fn();
620
778
  };
621
- const liftMessage = (item) => {
622
- const sender = asRecord$1(item.sender);
623
- const senderSpecifics = asRecord$1(sender.specifics);
624
- return {
625
- id: read$1(item.id),
626
- timestamp: isoToEpochMs$1(item.timestamp),
627
- chat_id: read$1(item.chat_id ?? null),
628
- object: read$1(item.object),
629
- provider: read$1(item.provider),
630
- text: read$1(item.text),
631
- sender_id: read$1(item.sender_id),
632
- is_deleted: toBool$1(item.is_deleted),
633
- is_delivered: toBool$1(item.is_delivered),
634
- is_edited: toBool$1(item.is_edited),
635
- is_event: toBool$1(item.is_event),
636
- is_hidden: toBool$1(item.is_hidden),
637
- is_mentionned: toBool$1(item.is_mentionned),
638
- is_pinned: toBool$1(item.is_pinned),
639
- is_seen: toBool$1(item.is_seen),
640
- is_sender: toBool$1(item.is_sender),
641
- sender_object: read$1(sender.object),
642
- sender_type: read$1(sender.type),
643
- sender_display_name: read$1(sender.display_name),
644
- sender_public_identifier: read$1(sender.public_identifier),
645
- sender_profile_url: read$1(sender.profile_url),
646
- sender_public_picture_url: read$1(sender.public_picture_url),
647
- sender_description: read$1(sender.description),
648
- sender_specifics_network_distance: read$1(senderSpecifics.network_distance),
649
- raw: JSON.stringify(item)
650
- };
779
+ const wrapStatement = (statement) => ({
780
+ run: (params) => invokeWithParams(statement.run.bind(statement), params),
781
+ get: (params) => invokeWithParams(statement.get.bind(statement), params),
782
+ all: (params) => invokeWithParams(statement.all.bind(statement), params)
783
+ });
784
+ const openRuntimeDatabase = (path, opts) => {
785
+ if (isBunRuntime()) {
786
+ const { Database } = loadModule("bun:sqlite");
787
+ return new Database(path, {
788
+ readonly: opts?.readonly,
789
+ strict: true
790
+ });
791
+ }
792
+ const { DatabaseSync } = loadModule("node:sqlite");
793
+ return opts?.readonly ? new DatabaseSync(path, { readOnly: true }) : new DatabaseSync(path);
651
794
  };
652
- const liftPost = (item) => {
653
- const analytics = asRecord$1(item.analytics);
654
- const author = asRecord$1(item.author);
655
- const authorSpecifics = asRecord$1(author.specifics);
656
- const permissions = asRecord$1(item.permissions);
657
- const poll = asRecord$1(item.poll);
658
- const quotedPost = asRecord$1(item.quoted_post);
659
- const quotedPostPermissions = asRecord$1(quotedPost.permissions);
660
- const quotedPostAuthor = asRecord$1(quotedPost.author);
661
- const quotedPostAuthorSpecifics = asRecord$1(quotedPostAuthor.specifics);
662
- const repostedBy = asRecord$1(item.reposted_by);
663
- return {
664
- id: read$1(item.id),
665
- created_at: isoToEpochMs$1(item.created_at),
666
- object: read$1(item.object),
667
- share_url: read$1(item.share_url),
668
- text: read$1(item.text),
669
- url: read$1(item.url),
670
- is_repost: toBool$1(item.is_repost),
671
- user_reacted: readReaction(item.user_reacted),
672
- comments_counter: read$1(item.comments_counter),
673
- reposts_counter: read$1(item.reposts_counter),
674
- analytics_impressions_counter: read$1(analytics.impressions_counter),
675
- author_id: read$1(author.id),
676
- author_object: read$1(author.object),
677
- author_type: read$1(author.type),
678
- author_display_name: read$1(author.display_name),
679
- author_public_identifier: read$1(author.public_identifier),
680
- author_profile_url: read$1(author.profile_url),
681
- author_public_picture_url: read$1(author.public_picture_url),
682
- author_description: read$1(author.description),
683
- author_specifics_network_distance: read$1(authorSpecifics.network_distance),
684
- permissions_can_post_comments: toBool$1(permissions.can_post_comments),
685
- permissions_can_react: toBool$1(permissions.can_react),
686
- permissions_can_share: toBool$1(permissions.can_share),
687
- poll_id: read$1(poll.id),
688
- poll_question: read$1(poll.question),
689
- poll_is_open: toBool$1(poll.is_open),
690
- poll_total_votes_count: read$1(poll.total_votes_count),
691
- quoted_post_id: read$1(quotedPost.id),
692
- quoted_post_object: read$1(quotedPost.object),
693
- quoted_post_created_at: isoToEpochMs$1(quotedPost.created_at),
694
- quoted_post_text: read$1(quotedPost.text),
695
- quoted_post_share_url: read$1(quotedPost.share_url),
696
- quoted_post_user_reacted: readReaction(quotedPost.user_reacted),
697
- quoted_post_comments_counter: read$1(quotedPost.comments_counter),
698
- quoted_post_reposts_counter: read$1(quotedPost.reposts_counter),
699
- quoted_post_permissions_can_post_comments: toBool$1(quotedPostPermissions.can_post_comments),
700
- quoted_post_permissions_can_react: toBool$1(quotedPostPermissions.can_react),
701
- quoted_post_permissions_can_share: toBool$1(quotedPostPermissions.can_share),
702
- quoted_post_author_id: read$1(quotedPostAuthor.id),
703
- quoted_post_author_object: read$1(quotedPostAuthor.object),
704
- quoted_post_author_type: read$1(quotedPostAuthor.type),
705
- quoted_post_author_display_name: read$1(quotedPostAuthor.display_name),
706
- quoted_post_author_public_identifier: read$1(quotedPostAuthor.public_identifier),
707
- quoted_post_author_profile_url: read$1(quotedPostAuthor.profile_url),
708
- quoted_post_author_public_picture_url: read$1(quotedPostAuthor.public_picture_url),
709
- quoted_post_author_description: read$1(quotedPostAuthor.description),
710
- quoted_post_author_specifics_network_distance: read$1(quotedPostAuthorSpecifics.network_distance),
711
- reposted_by_id: read$1(repostedBy.id),
712
- reposted_by_object: read$1(repostedBy.object),
713
- reposted_by_type: read$1(repostedBy.type),
714
- reposted_by_display_name: read$1(repostedBy.display_name),
715
- reposted_by_public_identifier: read$1(repostedBy.public_identifier),
716
- reposted_by_profile_url: read$1(repostedBy.profile_url),
717
- reposted_by_public_picture_url: read$1(repostedBy.public_picture_url),
718
- raw: JSON.stringify(item)
795
+ const openDatabase = (path, opts) => {
796
+ const database = openRuntimeDatabase(path, opts);
797
+ const exec = (sql) => {
798
+ database.exec(sql);
719
799
  };
720
- };
721
- const liftRequest = (item) => {
722
- const user = asRecord$1(item.user);
723
800
  return {
724
- id: read$1(item.id),
725
- created_at: isoToEpochMs$1(item.created_at),
726
- object: read$1(item.object),
727
- type: read$1(item.type ?? null),
728
- user_id: read$1(user.id),
729
- user_object: read$1(user.object),
730
- user_type: read$1(user.type),
731
- user_display_name: read$1(user.display_name),
732
- user_public_identifier: read$1(user.public_identifier),
733
- user_profile_url: read$1(user.profile_url),
734
- user_public_picture_url: read$1(user.public_picture_url),
735
- user_description: read$1(user.description),
736
- user_headline: userHeadline(user),
737
- raw: JSON.stringify(item)
801
+ exec,
802
+ prepare: (sql) => wrapStatement(database.prepare(sql)),
803
+ transaction: (fn) => {
804
+ exec("BEGIN");
805
+ try {
806
+ fn();
807
+ exec("COMMIT");
808
+ } catch (error) {
809
+ exec("ROLLBACK");
810
+ throw error;
811
+ }
812
+ },
813
+ close: () => {
814
+ database.close();
815
+ }
738
816
  };
739
817
  };
740
- const LINKEDIN_COLLECTIONS = [
741
- {
742
- key: "li_conversations",
743
- platform: "linkedin",
744
- requests: [{ path: "/chats" }],
745
- pagination: linkedinCursorPagination,
746
- pageSize: 20,
747
- sinceField: "last_message_timestamp",
748
- lift: liftConversation
818
+ //#endregion
819
+ //#region ../../packages/sync-engine/src/cache.ts
820
+ var NeverSyncedError = class extends Error {
821
+ name = "NeverSyncedError";
822
+ tables;
823
+ constructor(tables, message) {
824
+ super(message);
825
+ this.tables = tables;
826
+ }
827
+ };
828
+ const publicCacheTables = [
829
+ "x_tweets",
830
+ "x_followers",
831
+ "x_following",
832
+ "x_bookmarks",
833
+ "x_profiles",
834
+ "x_messages",
835
+ "li_conversations",
836
+ "li_messages",
837
+ "li_connections",
838
+ "li_posts",
839
+ "li_profiles",
840
+ "li_requests",
841
+ "sync_state"
842
+ ];
843
+ const rawCacheTables = publicCacheTables.filter((table) => table !== "sync_state").map((table) => `${table}_raw`);
844
+ const cacheTables = new Set([...publicCacheTables, ...rawCacheTables]);
845
+ const profileTables = new Set(["x_profiles_raw", "li_profiles_raw"]);
846
+ const sharedTables = new Set(["sync_state"]);
847
+ const platformTables = {
848
+ linkedin: new Set([...cacheTables].filter((table) => table.startsWith("li_"))),
849
+ x: new Set([...cacheTables].filter((table) => table.startsWith("x_")))
850
+ };
851
+ const sanitizeAccountId = (accountId) => accountId.replaceAll(/[^A-Za-z0-9._-]/g, "_");
852
+ const quoteIdentifier = (identifier) => `\`${identifier.replaceAll("`", "``")}\``;
853
+ const resolveCachePath = (path) => path === ":memory:" ? path : resolve(path);
854
+ const rawTableFor = (table) => table === "sync_state" ? table : `${table}_raw`;
855
+ const collectionForReadTable = (table) => table.endsWith("_raw") ? table.slice(0, -4) : table;
856
+ const physicalTableForCacheTable = (table) => table === "sync_state" || table.endsWith("_raw") ? table : rawTableFor(table);
857
+ const stringOrNull = (value) => typeof value === "string" ? value : null;
858
+ const numberOrNull = (value) => typeof value === "number" ? value : null;
859
+ const assertKnownTable = (table) => {
860
+ if (!cacheTables.has(table)) throw new Error(`Unknown cache table: ${table}`);
861
+ };
862
+ const tablesForPlatform = (platform) => new Set([...platformTables[platform], ...sharedTables]);
863
+ const knownTableTokens = (sql) => {
864
+ const tokens = sql.match(/\b[A-Za-z_][A-Za-z0-9_]*\b/g) ?? [];
865
+ const tables = [];
866
+ const seen = /* @__PURE__ */ new Set();
867
+ for (const token of tokens) {
868
+ const table = token.toLowerCase();
869
+ if (cacheTables.has(table) && !seen.has(table)) {
870
+ seen.add(table);
871
+ tables.push(table);
872
+ }
873
+ }
874
+ return tables;
875
+ };
876
+ const assertPlatformQuery = (sql, platform) => {
877
+ const allowedTables = tablesForPlatform(platform);
878
+ const rejectedTables = new Set(knownTableTokens(sql).filter((table) => !allowedTables.has(table)));
879
+ if (rejectedTables.size > 0) throw new Error(`Cache SQL for ${platform} cannot read ${[...rejectedTables].sort().join(", ")}.`);
880
+ };
881
+ const referencedTablesForRead = (sql, platform) => {
882
+ const allowedTables = platformTables[platform];
883
+ return knownTableTokens(sql).filter((table) => allowedTables.has(table));
884
+ };
885
+ const collectionShortName = (collection) => collection.replace(/^(x|li)_/, "");
886
+ const tableFreshnessPolicies = {
887
+ x_messages_raw: {
888
+ kind: "collection",
889
+ collection: "x_messages"
890
+ },
891
+ li_conversations: {
892
+ kind: "collection",
893
+ collection: "li_conversations",
894
+ commandCollection: "li_messages"
749
895
  },
750
- {
751
- key: "li_messages",
752
- platform: "linkedin",
753
- requests: [{ path: "/chats/:chat_id/messages" }],
754
- parentCollection: "li_conversations",
755
- parentIdToken: ":chat_id",
756
- pagination: linkedinCursorPagination,
757
- pageSize: 250,
758
- sinceField: "timestamp",
759
- lift: liftMessage
896
+ li_conversations_raw: {
897
+ kind: "collection",
898
+ collection: "li_conversations",
899
+ commandCollection: "li_messages"
760
900
  },
761
- {
762
- key: "li_connections",
763
- platform: "linkedin",
764
- requests: [{ path: "/users/connections" }],
765
- requiresFullInitialSync: true,
766
- pagination: linkedinRelationsCursorPagination,
767
- pageSize: 100,
768
- sinceField: "created_at",
769
- lift: liftConnection
901
+ li_profiles: {
902
+ kind: "platform",
903
+ platform: "linkedin"
770
904
  },
771
- {
772
- key: "li_posts",
773
- platform: "linkedin",
774
- requests: [{ path: "/users/:identifier/posts" }],
775
- ownIdToken: ":identifier",
776
- pagination: linkedinCursorPagination,
777
- pageSize: 50,
778
- sinceField: "created_at",
779
- lift: liftPost
905
+ li_profiles_raw: {
906
+ kind: "platform",
907
+ platform: "linkedin"
780
908
  },
781
- {
782
- key: "li_requests",
783
- platform: "linkedin",
784
- requests: [{
785
- path: "/users/me/relation-requests/received",
786
- bakedQuery: { type: "received" }
787
- }, {
788
- path: "/users/me/relation-requests/sent",
789
- bakedQuery: { type: "sent" }
790
- }],
791
- pagination: linkedinRelationsCursorPagination,
792
- pageSize: 100,
793
- lift: liftRequest
909
+ x_profiles: {
910
+ kind: "platform",
911
+ platform: "x"
912
+ },
913
+ x_profiles_raw: {
914
+ kind: "platform",
915
+ platform: "x"
794
916
  }
795
- ];
796
- //#endregion
797
- //#region ../../packages/sync-engine/src/collections/x.ts
798
- const xPagination = {
799
- style: "cursor",
800
- limitParam: "max_results",
801
- cursorParam: "pagination_token",
802
- cursorPath: "meta.next_token",
803
- itemsPath: "data"
804
917
  };
805
- const defaultDMEventFields = "attachments,created_at,dm_conversation_id,entities,event_type,id,participant_ids,referenced_tweets,sender_id,text";
806
- const defaultDMExpansions = "attachments.media_keys,participant_ids,referenced_tweets.id,sender_id";
807
- const defaultDMUserFields = "affiliation,connection_status,created_at,description,entities,id,location,most_recent_tweet_id,name,pinned_tweet_id,profile_banner_url,profile_image_url,protected,public_metrics,receives_your_dm,subscription_type,url,username,verified,verified_type,withheld";
808
- const asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
809
- const read = (value) => value === null || value === void 0 ? null : value;
810
- const isoToEpochMs = (value) => {
811
- if (typeof value === "number") return Number.isFinite(value) ? value : null;
812
- if (typeof value !== "string") return null;
813
- const epochMs = Date.parse(value);
814
- return Number.isNaN(epochMs) ? null : epochMs;
918
+ const freshnessPolicyForTable = (table) => tableFreshnessPolicies[table] ?? {
919
+ kind: "collection",
920
+ collection: collectionForReadTable(table)
815
921
  };
816
- const toBool = (value) => {
817
- if (typeof value !== "boolean") return null;
818
- return value ? 1 : 0;
922
+ const platformCollectionPrefix = (platform) => platform === "linkedin" ? "li_" : "x_";
923
+ const syncCommandForPolicy = (platform, policy) => {
924
+ if (policy.kind === "platform") return `social ${policy.platform} sync`;
925
+ return `social ${platform} sync ${collectionShortName(policy.commandCollection ?? policy.collection)}`;
819
926
  };
820
- const stringifyJSON$1 = (value) => value === null || value === void 0 ? null : JSON.stringify(value);
821
- const liftTweetColumns = (item) => {
822
- const publicMetrics = asRecord(item.public_metrics);
823
- return {
824
- id: read(item.id),
825
- created_at: isoToEpochMs(item.created_at),
826
- author_id: read(item.author_id),
827
- conversation_id: read(item.conversation_id),
828
- in_reply_to_user_id: read(item.in_reply_to_user_id),
829
- lang: read(item.lang),
830
- reply_settings: read(item.reply_settings),
831
- text: read(item.text),
832
- url: read(item.url),
833
- paid_partnership: toBool(item.paid_partnership),
834
- possibly_sensitive: toBool(item.possibly_sensitive),
835
- like_count: read(publicMetrics.like_count),
836
- retweet_count: read(publicMetrics.retweet_count),
837
- reply_count: read(publicMetrics.reply_count),
838
- quote_count: read(publicMetrics.quote_count),
839
- bookmark_count: read(publicMetrics.bookmark_count),
840
- impression_count: read(publicMetrics.impression_count)
841
- };
927
+ const missingNameForPolicy = (table, policy) => policy.kind === "platform" ? table : policy.collection;
928
+ const formatList = (items) => {
929
+ if (items.length <= 1) return items[0] ?? "";
930
+ if (items.length === 2) return `${items[0]} and ${items[1]}`;
931
+ return `${items.slice(0, -1).join(", ")}, and ${items.at(-1)}`;
842
932
  };
843
- const liftTweet = (item) => ({
844
- ...liftTweetColumns(item),
845
- raw: JSON.stringify(item)
846
- });
847
- const liftBookmark = (item) => {
848
- const article = asRecord(item.article);
849
- const noteTweet = asRecord(item.note_tweet);
850
- return {
851
- ...liftTweetColumns(item),
852
- article_cover_media: read(article.cover_media),
853
- article_plain_text: read(article.plain_text),
854
- article_preview_text: read(article.preview_text),
855
- article_title: read(article.title),
856
- note_tweet_text: read(noteTweet.text),
857
- raw: JSON.stringify(item)
858
- };
933
+ const collectionLastSynced = (db, collection) => {
934
+ return numberOrNull(db.prepare("SELECT last_synced FROM sync_state WHERE collection = :collection").get({ collection })?.last_synced);
859
935
  };
860
- const liftDMEvent = (item) => ({
861
- id: read(item.id),
862
- created_at: isoToEpochMs(item.created_at),
863
- dm_conversation_id: read(item.dm_conversation_id),
864
- event_type: read(item.event_type),
865
- sender_id: read(item.sender_id),
866
- text: read(item.text),
867
- participant_ids: stringifyJSON$1(item.participant_ids),
868
- attachments: stringifyJSON$1(item.attachments),
869
- entities: stringifyJSON$1(item.entities),
870
- referenced_tweets: stringifyJSON$1(item.referenced_tweets),
871
- raw: JSON.stringify(item)
872
- });
873
- const liftXUser = (item) => {
874
- const affiliation = asRecord(item.affiliation);
875
- const publicMetrics = asRecord(item.public_metrics);
876
- const subscription = asRecord(item.subscription);
936
+ const platformLastSynced = (db, platform) => {
937
+ return numberOrNull(db.prepare("SELECT MAX(last_synced) AS last_synced FROM sync_state WHERE collection GLOB :glob").get({ glob: `${platformCollectionPrefix(platform)}*` })?.last_synced);
938
+ };
939
+ const lastSyncedForPolicy = (db, policy) => policy.kind === "platform" ? platformLastSynced(db, policy.platform) : collectionLastSynced(db, policy.collection);
940
+ const missingFreshnessForTables = (tables) => {
941
+ const missing = [];
942
+ const seen = /* @__PURE__ */ new Set();
943
+ for (const table of tables) {
944
+ if (table.lastSynced != null) continue;
945
+ const key = `${table.missingName}\0${table.command}`;
946
+ if (!seen.has(key)) {
947
+ seen.add(key);
948
+ missing.push({
949
+ name: table.missingName,
950
+ command: table.command
951
+ });
952
+ }
953
+ }
954
+ return missing;
955
+ };
956
+ const neverSyncedMessage = (missing) => {
957
+ const names = missing.map((item) => item.name);
958
+ const commands = missing.map((item) => `\`${item.command}\``);
959
+ return `No synced ${formatList(names)} yet — run ${formatList(commands)} first.`;
960
+ };
961
+ const assertKnownProfileTable = (table) => {
962
+ assertKnownTable(table);
963
+ if (!profileTables.has(physicalTableForCacheTable(table))) throw new Error(`Unknown profile table: ${table}`);
964
+ };
965
+ const cacheDescriptionNotes = ["time columns (*_at, muted_until, synced_at) are epoch ms; booleans are 0/1", "each view has a <name>_raw twin holding the upstream JSON: json_extract(raw,'$.field')"];
966
+ const metadataForPlatform = (platform) => {
967
+ const platforms = platform ? [platform] : ["linkedin", "x"];
877
968
  return {
878
- id: read(item.id),
879
- created_at: isoToEpochMs(item.created_at),
880
- username: read(item.username),
881
- name: read(item.name),
882
- description: read(item.description),
883
- location: read(item.location),
884
- url: read(item.url),
885
- is_identity_verified: toBool(item.is_identity_verified),
886
- protected: toBool(item.protected),
887
- verified: toBool(item.verified),
888
- verified_type: read(item.verified_type),
889
- subscription_type: read(item.subscription_type),
890
- parody: toBool(item.parody),
891
- most_recent_tweet_id: read(item.most_recent_tweet_id),
892
- pinned_tweet_id: read(item.pinned_tweet_id),
893
- profile_banner_url: read(item.profile_banner_url),
894
- profile_image_url: read(item.profile_image_url),
895
- affiliation_badge_url: read(affiliation.badge_url),
896
- affiliation_description: read(affiliation.description),
897
- affiliation_url: read(affiliation.url),
898
- subscription_subscribes_to_you: toBool(subscription.subscribes_to_you),
899
- followers_count: read(publicMetrics.followers_count),
900
- following_count: read(publicMetrics.following_count),
901
- tweet_count: read(publicMetrics.tweet_count),
902
- listed_count: read(publicMetrics.listed_count),
903
- like_count: read(publicMetrics.like_count),
904
- media_count: read(publicMetrics.media_count),
905
- raw: JSON.stringify(item)
969
+ joins: platforms.flatMap((item) => CACHE_METADATA_BY_PLATFORM[item].joins),
970
+ enums: Object.assign({}, ...platforms.map((item) => CACHE_METADATA_BY_PLATFORM[item].enums)),
971
+ indexed: Object.assign({}, ...platforms.map((item) => CACHE_METADATA_BY_PLATFORM[item].indexed))
906
972
  };
907
973
  };
908
- //#endregion
909
- //#region ../../packages/sync-engine/src/collections/index.ts
910
- const COLLECTIONS = [...[
911
- {
912
- key: "x_tweets",
913
- platform: "x",
914
- requests: [{ path: "/users/:id/tweets" }],
915
- ownIdToken: ":id",
916
- pagination: xPagination,
917
- pageSize: 100,
918
- sinceField: "created_at",
919
- lift: liftTweet
974
+ const cachePath = (args) => join(homedir(), ".social", "cache", `${args.platform}-${sanitizeAccountId(args.accountId)}.db`);
975
+ const openCacheAt = (path, opts) => {
976
+ const dbPath = resolveCachePath(path);
977
+ if (opts?.readonly) {
978
+ const db = openDatabase(dbPath, { readonly: true });
979
+ assertAtLatest(db);
980
+ return createCache(dbPath, db);
981
+ }
982
+ if (dbPath !== ":memory:") mkdirSync(dirname(dbPath), { recursive: true });
983
+ const db = openDatabase(dbPath);
984
+ const result = migrate(db);
985
+ if (result.applied > 0) opts?.onMigrate?.(result);
986
+ return createCache(dbPath, db);
987
+ };
988
+ const openCache = (args) => openCacheAt(cachePath(args), {
989
+ readonly: args.readonly,
990
+ onMigrate: args.onMigrate
991
+ });
992
+ const createCache = (path, db) => ({
993
+ upsert: (table, rows, syncedAt = Date.now()) => {
994
+ if (rows.length === 0) return;
995
+ assertKnownTable(table);
996
+ const physicalTable = physicalTableForCacheTable(table);
997
+ const columns = [...Object.keys(rows[0] ?? {}).filter((column) => column !== "synced_at"), "synced_at"];
998
+ const updateColumns = columns.filter((column) => column !== "id");
999
+ const statement = db.prepare(`INSERT INTO ${quoteIdentifier(physicalTable)} (${columns.map(quoteIdentifier).join(", ")}) VALUES (${columns.map((column) => `:${column}`).join(", ")}) ON CONFLICT(id) DO UPDATE SET ${updateColumns.map((column) => `${quoteIdentifier(column)}=excluded.${quoteIdentifier(column)}`).join(", ")}`);
1000
+ db.transaction(() => {
1001
+ for (const row of rows) statement.run({
1002
+ ...row,
1003
+ synced_at: syncedAt
1004
+ });
1005
+ });
920
1006
  },
921
- {
922
- key: "x_followers",
923
- platform: "x",
924
- requests: [{ path: "/users/:id/followers" }],
925
- ownIdToken: ":id",
926
- requiresFullInitialSync: true,
927
- pagination: xPagination,
928
- pageSize: 1e3,
929
- lift: liftXUser
1007
+ upsertProfiles: (table, rows) => {
1008
+ if (rows.length === 0) return;
1009
+ assertKnownProfileTable(table);
1010
+ const physicalTable = physicalTableForCacheTable(table);
1011
+ const rowsByProviderId = /* @__PURE__ */ new Map();
1012
+ for (const row of rows) rowsByProviderId.set(row.providerId, row);
1013
+ const columns = [
1014
+ "provider_id",
1015
+ "username",
1016
+ "display_name",
1017
+ "avatar_url",
1018
+ "headline",
1019
+ "raw",
1020
+ "synced_at"
1021
+ ];
1022
+ const quotedTable = quoteIdentifier(physicalTable);
1023
+ const nullableColumns = [
1024
+ "username",
1025
+ "avatar_url",
1026
+ "headline"
1027
+ ];
1028
+ const lastWriteColumns = [
1029
+ "display_name",
1030
+ "raw",
1031
+ "synced_at"
1032
+ ];
1033
+ const updateAssignments = [...nullableColumns.map((column) => `${quoteIdentifier(column)}=COALESCE(excluded.${quoteIdentifier(column)}, ${quotedTable}.${quoteIdentifier(column)})`), ...lastWriteColumns.map((column) => `${quoteIdentifier(column)}=excluded.${quoteIdentifier(column)}`)].join(", ");
1034
+ const statement = db.prepare(`INSERT INTO ${quotedTable} (${columns.map(quoteIdentifier).join(", ")}) VALUES (${columns.map((column) => `:${column}`).join(", ")}) ON CONFLICT(provider_id) DO UPDATE SET ${updateAssignments}`);
1035
+ const syncedAt = Date.now();
1036
+ db.transaction(() => {
1037
+ for (const row of rowsByProviderId.values()) statement.run({
1038
+ provider_id: row.providerId,
1039
+ username: row.username,
1040
+ display_name: row.displayName,
1041
+ avatar_url: row.avatarURL,
1042
+ headline: row.headline,
1043
+ raw: JSON.stringify(row.raw),
1044
+ synced_at: syncedAt
1045
+ });
1046
+ });
930
1047
  },
931
- {
932
- key: "x_following",
933
- platform: "x",
934
- requests: [{ path: "/users/:id/following" }],
935
- ownIdToken: ":id",
936
- requiresFullInitialSync: true,
937
- pagination: xPagination,
938
- pageSize: 1e3,
939
- lift: liftXUser
1048
+ getCursor: (collection) => {
1049
+ const row = db.prepare("SELECT cursor, last_synced, object_count FROM sync_state WHERE collection = :collection").get({ collection });
1050
+ if (!row) return;
1051
+ return {
1052
+ cursor: stringOrNull(row.cursor),
1053
+ lastSynced: numberOrNull(row.last_synced),
1054
+ objectCount: numberOrNull(row.object_count)
1055
+ };
940
1056
  },
941
- {
942
- key: "x_bookmarks",
943
- platform: "x",
944
- requests: [{ path: "/users/:id/bookmarks" }],
945
- ownIdToken: ":id",
946
- pagination: xPagination,
947
- pageSize: 100,
948
- lift: liftBookmark
1057
+ setCursor: (collection, cursor, objectCount, lastSynced = Date.now()) => {
1058
+ db.prepare("INSERT INTO sync_state (collection, cursor, last_synced, object_count) VALUES (:collection, :cursor, :last_synced, :object_count) ON CONFLICT(collection) DO UPDATE SET cursor=excluded.cursor, last_synced=excluded.last_synced, object_count=excluded.object_count").run({
1059
+ collection,
1060
+ cursor,
1061
+ last_synced: lastSynced,
1062
+ object_count: objectCount
1063
+ });
949
1064
  },
950
- {
951
- key: "x_messages",
952
- table: "x_raw_messages",
953
- platform: "x",
954
- requests: [{
955
- path: "/dm_events",
956
- bakedQuery: {
957
- "dm_event.fields": defaultDMEventFields,
958
- expansions: defaultDMExpansions,
959
- "user.fields": defaultDMUserFields
1065
+ reset: ({ tables, collections }) => {
1066
+ for (const table of tables) assertKnownTable(table);
1067
+ const results = [];
1068
+ db.transaction(() => {
1069
+ for (const table of tables) {
1070
+ const physicalTable = physicalTableForCacheTable(table);
1071
+ const result = db.prepare(`DELETE FROM ${quoteIdentifier(physicalTable)}`).run();
1072
+ results.push({
1073
+ table: physicalTable,
1074
+ rowsDeleted: result?.changes ?? 0
1075
+ });
960
1076
  }
961
- }],
962
- pagination: xPagination,
963
- pageSize: 100,
964
- sinceField: "created_at",
965
- lift: liftDMEvent
1077
+ const deleteState = db.prepare("DELETE FROM sync_state WHERE collection = :collection");
1078
+ for (const collection of collections) deleteState.run({ collection });
1079
+ });
1080
+ return results;
1081
+ },
1082
+ query: (sql, opts) => {
1083
+ if (opts?.platform) assertPlatformQuery(sql, opts.platform);
1084
+ return db.prepare(sql).all();
1085
+ },
1086
+ read: (sql, opts) => {
1087
+ assertPlatformQuery(sql, opts.platform);
1088
+ const tables = referencedTablesForRead(sql, opts.platform).map((name) => {
1089
+ const policy = freshnessPolicyForTable(name);
1090
+ return {
1091
+ name,
1092
+ missingName: missingNameForPolicy(name, policy),
1093
+ command: syncCommandForPolicy(opts.platform, policy),
1094
+ lastSynced: lastSyncedForPolicy(db, policy)
1095
+ };
1096
+ });
1097
+ const missingFreshness = missingFreshnessForTables(tables);
1098
+ if (missingFreshness.length > 0) throw new NeverSyncedError(missingFreshness.map((item) => item.name), neverSyncedMessage(missingFreshness));
1099
+ return {
1100
+ rows: db.prepare(sql).all(),
1101
+ tables: tables.map(({ name, lastSynced }) => ({
1102
+ name,
1103
+ lastSynced
1104
+ }))
1105
+ };
1106
+ },
1107
+ describe: (opts) => {
1108
+ const allowedTables = opts?.platform ? tablesForPlatform(opts.platform) : void 0;
1109
+ const metadata = metadataForPlatform(opts?.platform);
1110
+ const tables = db.prepare("SELECT name FROM sqlite_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%' ORDER BY name").all().filter((table) => !String(table.name).endsWith("_raw")).filter((table) => allowedTables === void 0 || allowedTables.has(String(table.name)));
1111
+ const lastSyncedForTable = (name) => {
1112
+ if (name === "sync_state") return null;
1113
+ return lastSyncedForPolicy(db, freshnessPolicyForTable(name));
1114
+ };
1115
+ const columnsForTable = (name) => db.prepare(`PRAGMA table_info(${quoteIdentifier(name)})`).all().map((row) => String(row.name)).join(", ");
1116
+ return {
1117
+ path,
1118
+ notes: cacheDescriptionNotes,
1119
+ joins: metadata.joins,
1120
+ enums: metadata.enums,
1121
+ tables: tables.map((table) => {
1122
+ const name = String(table.name);
1123
+ const rowCount = db.prepare(`SELECT count(*) AS rows FROM ${quoteIdentifier(name)}`).get();
1124
+ const syncedAt = lastSyncedForTable(name);
1125
+ return {
1126
+ name,
1127
+ rows: Number(rowCount?.rows ?? 0),
1128
+ synced_at: syncedAt,
1129
+ query_ready: syncedAt != null,
1130
+ columns: columnsForTable(name),
1131
+ indexed: metadata.indexed[name] ?? []
1132
+ };
1133
+ })
1134
+ };
1135
+ },
1136
+ close: () => {
1137
+ db.close();
966
1138
  }
967
- ], ...LINKEDIN_COLLECTIONS];
968
- const collectionByKey = (key) => COLLECTIONS.find((collection) => collection.key === key);
969
- const collectionsFor = (platform) => COLLECTIONS.filter((collection) => collection.platform === platform);
970
- const syncableCollectionsFor = (platform) => {
971
- const parentKeys = new Set(COLLECTIONS.map((collection) => collection.parentCollection).filter(Boolean));
972
- return collectionsFor(platform).filter((collection) => !parentKeys.has(collection.key));
973
- };
974
- const physicalTableFor = (collection) => collection.table ?? collection.key;
975
- const resetTablesFor = (collection) => {
976
- if (!collection.parentCollection) return [collection];
977
- const parent = collectionByKey(collection.parentCollection);
978
- return parent ? [collection, parent] : [collection];
979
- };
980
- const resetPhysicalTablesFor = (collection) => resetTablesFor(collection).map(physicalTableFor);
1139
+ });
981
1140
  //#endregion
982
1141
  //#region ../../packages/sync-engine/src/cost.ts
983
1142
  const creditsFromHeader = (headers) => {
@@ -987,7 +1146,7 @@ const creditsFromHeader = (headers) => {
987
1146
  };
988
1147
  //#endregion
989
1148
  //#region ../../packages/sync-engine/src/profiles.ts
990
- const profileTableFor = (platform) => platform === "x" ? "x_profiles" : "li_profiles";
1149
+ const profileTableFor = (platform) => platform === "x" ? "x_profiles_raw" : "li_profiles_raw";
991
1150
  //#endregion
992
1151
  //#region ../../node_modules/.bun/drizzle-orm@0.43.1+ae5d55cf414a93be/node_modules/drizzle-orm/entity.js
993
1152
  const entityKind = Symbol.for("drizzle:entityKind");
@@ -3748,7 +3907,7 @@ const rawColumns = () => ({
3748
3907
  raw: text$1("raw").notNull(),
3749
3908
  syncedAt: integer$2("synced_at").notNull()
3750
3909
  });
3751
- sqliteTable("x_tweets", {
3910
+ sqliteTable("x_tweets_raw", {
3752
3911
  id: idColumn(),
3753
3912
  createdAt: integer$2("created_at"),
3754
3913
  authorId: text$1("author_id"),
@@ -3772,7 +3931,7 @@ sqliteTable("x_tweets", {
3772
3931
  index$1("x_tweets_author_id_created_at_idx").on(table.authorId, table.createdAt),
3773
3932
  index$1("x_tweets_conversation_id_created_at_idx").on(table.conversationId, table.createdAt)
3774
3933
  ]);
3775
- sqliteTable("x_bookmarks", {
3934
+ sqliteTable("x_bookmarks_raw", {
3776
3935
  id: idColumn(),
3777
3936
  createdAt: integer$2("created_at"),
3778
3937
  authorId: text$1("author_id"),
@@ -3803,14 +3962,14 @@ sqliteTable("x_bookmarks", {
3803
3962
  ]);
3804
3963
  const profileColumns = () => ({
3805
3964
  providerId: text$1("provider_id").primaryKey(),
3806
- handle: text$1("handle"),
3965
+ username: text$1("username"),
3807
3966
  displayName: text$1("display_name").notNull(),
3808
3967
  avatarURL: text$1("avatar_url"),
3809
3968
  headline: text$1("headline"),
3810
3969
  ...rawColumns()
3811
3970
  });
3812
- sqliteTable("x_profiles", profileColumns(), (table) => [index$1("x_profiles_handle_idx").on(table.handle)]);
3813
- sqliteTable("x_raw_messages", {
3971
+ sqliteTable("x_profiles_raw", profileColumns(), (table) => [index$1("x_profiles_username_idx").on(table.username)]);
3972
+ sqliteTable("x_messages_raw", {
3814
3973
  id: idColumn(),
3815
3974
  createdAt: integer$2("created_at"),
3816
3975
  dmConversationId: text$1("dm_conversation_id"),
@@ -3857,21 +4016,21 @@ const xUserColumns = () => ({
3857
4016
  mediaCount: integer$2("media_count"),
3858
4017
  ...rawColumns()
3859
4018
  });
3860
- sqliteTable("x_followers", xUserColumns(), (table) => [
4019
+ sqliteTable("x_followers_raw", xUserColumns(), (table) => [
3861
4020
  index$1("x_followers_created_at_idx").on(table.createdAt),
3862
4021
  index$1("x_followers_synced_at_idx").on(table.syncedAt),
3863
4022
  index$1("x_followers_username_idx").on(table.username),
3864
4023
  index$1("x_followers_verified_idx").on(table.verified),
3865
4024
  index$1("x_followers_verified_type_idx").on(table.verifiedType)
3866
4025
  ]);
3867
- sqliteTable("x_following", xUserColumns(), (table) => [
4026
+ sqliteTable("x_following_raw", xUserColumns(), (table) => [
3868
4027
  index$1("x_following_created_at_idx").on(table.createdAt),
3869
4028
  index$1("x_following_synced_at_idx").on(table.syncedAt),
3870
4029
  index$1("x_following_username_idx").on(table.username),
3871
4030
  index$1("x_following_verified_idx").on(table.verified),
3872
4031
  index$1("x_following_verified_type_idx").on(table.verifiedType)
3873
4032
  ]);
3874
- sqliteTable("li_connections", {
4033
+ sqliteTable("li_connections_raw", {
3875
4034
  id: idColumn(),
3876
4035
  createdAt: integer$2("created_at"),
3877
4036
  object: text$1("object"),
@@ -3893,8 +4052,8 @@ sqliteTable("li_connections", {
3893
4052
  index$1("li_connections_user_public_identifier_idx").on(table.userPublicIdentifier),
3894
4053
  index$1("li_connections_user_display_name_idx").on(table.userDisplayName)
3895
4054
  ]);
3896
- sqliteTable("li_profiles", profileColumns(), (table) => [index$1("li_profiles_handle_idx").on(table.handle)]);
3897
- sqliteTable("li_conversations", {
4055
+ sqliteTable("li_profiles_raw", profileColumns(), (table) => [index$1("li_profiles_username_idx").on(table.username)]);
4056
+ sqliteTable("li_conversations_raw", {
3898
4057
  id: idColumn(),
3899
4058
  createdAt: integer$2("created_at"),
3900
4059
  lastMessageTimestamp: integer$2("last_message_timestamp"),
@@ -3931,7 +4090,7 @@ sqliteTable("li_conversations", {
3931
4090
  index$1("li_conversations_user_display_name_idx").on(table.userDisplayName),
3932
4091
  index$1("li_conversations_is_archived_last_message_timestamp_idx").on(table.isArchived, table.lastMessageTimestamp)
3933
4092
  ]);
3934
- sqliteTable("li_messages", {
4093
+ sqliteTable("li_messages_raw", {
3935
4094
  id: idColumn(),
3936
4095
  timestamp: integer$2("timestamp"),
3937
4096
  chatId: text$1("chat_id"),
@@ -3962,7 +4121,7 @@ sqliteTable("li_messages", {
3962
4121
  index$1("li_messages_chat_id_timestamp_idx").on(table.chatId, table.timestamp),
3963
4122
  index$1("li_messages_sender_id_timestamp_idx").on(table.senderId, table.timestamp)
3964
4123
  ]);
3965
- sqliteTable("li_posts", {
4124
+ sqliteTable("li_posts_raw", {
3966
4125
  id: idColumn(),
3967
4126
  createdAt: integer$2("created_at"),
3968
4127
  object: text$1("object"),
@@ -4023,7 +4182,7 @@ sqliteTable("li_posts", {
4023
4182
  index$1("li_posts_author_id_created_at_idx").on(table.authorId, table.createdAt),
4024
4183
  index$1("li_posts_author_public_identifier_created_at_idx").on(table.authorPublicIdentifier, table.createdAt)
4025
4184
  ]);
4026
- sqliteTable("li_requests", {
4185
+ sqliteTable("li_requests_raw", {
4027
4186
  id: idColumn(),
4028
4187
  createdAt: integer$2("created_at"),
4029
4188
  object: text$1("object"),
@@ -4053,19 +4212,31 @@ sqliteTable("sync_state", {
4053
4212
  });
4054
4213
  //#endregion
4055
4214
  //#region ../../packages/sync-engine/src/since.ts
4056
- const durationPattern = /^(\d+):(hours|days|weeks)$/;
4057
- const isoPattern = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}Z)?$/;
4058
- const unitMs = {
4059
- hours: 36e5,
4060
- days: 864e5,
4061
- weeks: 6048e5
4062
- };
4063
- const parseSince = (input, now = Date.now()) => {
4215
+ const isoDatePattern = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/;
4216
+ const isoDateTimePattern = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])T([01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d{1,3})?)?(?:Z|[+-]([01]\d|2[0-3]):[0-5]\d)?$/;
4217
+ const timezonePattern = /(Z|[+-]\d{2}:\d{2})$/;
4218
+ const invalidSinceMessage = "Invalid --since value. Use an ISO 8601 date like 2026-05-04 or datetime like 2026-05-04T00:00:00Z.";
4219
+ const daysInMonth = (year, month) => {
4220
+ if (month === 2) return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28;
4221
+ return [
4222
+ 4,
4223
+ 6,
4224
+ 9,
4225
+ 11
4226
+ ].includes(month) ? 30 : 31;
4227
+ };
4228
+ const hasValidDate = (value) => {
4229
+ const year = Number.parseInt(value.slice(0, 4), 10);
4230
+ const month = Number.parseInt(value.slice(5, 7), 10);
4231
+ return Number.parseInt(value.slice(8, 10), 10) <= daysInMonth(year, month);
4232
+ };
4233
+ const parseSince = (input) => {
4064
4234
  const value = input.trim();
4065
- const duration = durationPattern.exec(value);
4066
- if (duration != null) return now - Number.parseInt(duration[1] ?? "", 10) * unitMs[duration[2]];
4067
- const parsed = isoPattern.test(value) ? Date.parse(value) : NaN;
4068
- if (Number.isNaN(parsed)) throw new Error("Invalid --since value. Use an ISO 8601 date/datetime like 2026-06-01 or 2026-06-01T00:00:00Z, or a compact duration like 2:days, 3:weeks, or 5:hours.");
4235
+ const isISODate = isoDatePattern.test(value);
4236
+ const isISODateTime = isoDateTimePattern.test(value);
4237
+ const normalized = isISODateTime && !timezonePattern.test(value) ? `${value}Z` : value;
4238
+ const parsed = (isISODate || isISODateTime) && hasValidDate(value) ? Date.parse(normalized) : NaN;
4239
+ if (Number.isNaN(parsed)) throw new Error(invalidSinceMessage);
4069
4240
  return parsed;
4070
4241
  };
4071
4242
  //#endregion
@@ -8722,6 +8893,7 @@ var SyncPageError = class extends Error {
8722
8893
  pagesCompleted;
8723
8894
  objectsUpserted;
8724
8895
  creditsSpent;
8896
+ cursorPersisted;
8725
8897
  cause;
8726
8898
  constructor(context, cause) {
8727
8899
  super(`Failed to sync ${context.collectionKey} page at ${context.path}`);
@@ -8737,13 +8909,31 @@ var SyncPageError = class extends Error {
8737
8909
  this.pagesCompleted = context.pagesCompleted;
8738
8910
  this.objectsUpserted = context.objectsUpserted;
8739
8911
  this.creditsSpent = context.creditsSpent;
8912
+ this.cursorPersisted = context.cursorPersisted;
8740
8913
  this.cause = cause;
8741
8914
  }
8742
8915
  };
8916
+ const PARTIAL_SYNC_CURSOR_PREFIX = "partial-sync-v1:";
8743
8917
  const itemsSchema = array$1(record$1(string$1(), unknown$1()));
8744
8918
  const cursorSchema = union$1([string$1(), _null$1()]);
8745
8919
  const totalCountSchema = number$1().int().nonnegative();
8746
8920
  const isRecord$5 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
8921
+ const validPartialSyncCursor = (value) => value.version === 1 && Number.isInteger(value.requestIndex) && typeof value.requestIndex === "number" && value.requestIndex >= 0 && typeof value.cursor === "string" && (typeof value.checkpoint === "string" || value.checkpoint === null) && (typeof value.newestId === "string" || value.newestId === null) && (typeof value.since === "number" || value.since === null);
8922
+ const decodePartialSyncCursor = (value) => {
8923
+ if (!value?.startsWith(PARTIAL_SYNC_CURSOR_PREFIX)) return;
8924
+ try {
8925
+ const parsed = JSON.parse(value.slice(16));
8926
+ return isRecord$5(parsed) && validPartialSyncCursor(parsed) ? parsed : void 0;
8927
+ } catch {
8928
+ return;
8929
+ }
8930
+ };
8931
+ const encodePartialSyncCursor = (cursor) => `${PARTIAL_SYNC_CURSOR_PREFIX}${JSON.stringify(cursor)}`;
8932
+ const isRateLimitError = (error) => isRecord$5(error) && error.status === 429;
8933
+ const retryAfterSecondsFromRateLimitError = (error) => {
8934
+ if (!(isRateLimitError(error) && isRecord$5(error))) return;
8935
+ return typeof error.retryAfterSeconds === "number" && Number.isFinite(error.retryAfterSeconds) && error.retryAfterSeconds >= 0 ? error.retryAfterSeconds : void 0;
8936
+ };
8747
8937
  const getByPath = (obj, path) => {
8748
8938
  if (path === void 0) return;
8749
8939
  let current = obj;
@@ -8877,18 +9067,62 @@ const mergeStoppedReason = (current, next) => {
8877
9067
  return "complete";
8878
9068
  };
8879
9069
  const walkRequest = async (args) => {
8880
- const checkpoint = args.useCheckpoint ? args.deps.cache.getCursor(args.collection.key)?.cursor ?? null : null;
9070
+ const storedCursor = args.deps.cache.getCursor(args.collection.key)?.cursor ?? null;
9071
+ const storedPartialCursor = decodePartialSyncCursor(storedCursor);
9072
+ const checkpoint = args.resumeCursor?.checkpoint ?? (args.useCheckpoint ? storedPartialCursor?.checkpoint ?? storedCursor : null);
8881
9073
  const ids = [];
8882
9074
  let state = {
8883
9075
  checkpoint,
8884
- cursor: null,
9076
+ cursor: args.resumeCursor?.cursor ?? null,
8885
9077
  offset: 0,
8886
- newestId: null,
8887
- firstPage: true
9078
+ newestId: args.resumeCursor?.newestId ?? null,
9079
+ firstPage: args.resumeCursor === void 0
8888
9080
  };
8889
9081
  let pages = 0;
8890
9082
  let upserted = 0;
8891
9083
  let stoppedReason = "complete";
9084
+ const rateLimitNow = args.rateLimit?.now ?? Date.now;
9085
+ const rateLimitSleep = args.rateLimit?.sleep ?? sleep;
9086
+ const rateLimitMaxRetries = args.rateLimit?.maxRetries ?? 25;
9087
+ const pageRequestOptions = {
9088
+ headers: args.deps.accountHeaders,
9089
+ ...args.rateLimit?.timeoutSeconds === void 0 ? {} : { retry: { limit: 0 } }
9090
+ };
9091
+ const waitForRateLimit = async (error) => {
9092
+ if (!(args.rateLimit && args.rateLimitState?.deadlineMs !== void 0)) return false;
9093
+ const retryCount = args.rateLimitState.retryCount + 1;
9094
+ const retryAfterSeconds = args.rateLimit.retryAfterSecondsFor(error, retryCount);
9095
+ if (!(args.rateLimit.isRateLimitError?.(error) ?? retryAfterSeconds !== void 0)) return false;
9096
+ const delaySeconds = retryAfterSeconds ?? args.rateLimit.fallbackDelaySecondsForRetry?.(retryCount);
9097
+ if (delaySeconds === void 0 || retryCount > rateLimitMaxRetries) return false;
9098
+ const remainingMs = args.rateLimitState.deadlineMs - rateLimitNow();
9099
+ if (delaySeconds * 1e3 > remainingMs) return false;
9100
+ args.rateLimitState.retryCount = retryCount;
9101
+ await args.rateLimit.onRetry?.({
9102
+ delaySeconds,
9103
+ retryCount
9104
+ });
9105
+ await rateLimitSleep(delaySeconds * 1e3);
9106
+ return true;
9107
+ };
9108
+ const persistPartialCursor = (error) => {
9109
+ const isRateLimit = args.rateLimit?.isRateLimitError?.(error) ?? args.rateLimit?.retryAfterSecondsFor(error, 1) !== void 0;
9110
+ if (!(args.allowPartialResume && args.rateLimit && isRateLimit) || args.collection.pagination.style !== "cursor" || !state.cursor) return false;
9111
+ args.writePendingCursor({
9112
+ collection: args.collection.key,
9113
+ cursor: encodePartialSyncCursor({
9114
+ version: 1,
9115
+ requestIndex: args.requestIndex,
9116
+ cursor: state.cursor,
9117
+ checkpoint,
9118
+ newestId: state.newestId,
9119
+ since: args.since ?? null
9120
+ }),
9121
+ objectCount: upserted,
9122
+ lastSynced: null
9123
+ });
9124
+ return true;
9125
+ };
8892
9126
  while (true) {
8893
9127
  if (args.pageDelayState.pagesCompleted > 0) {
8894
9128
  const delay = pageDelayFor(args.deps.pageDelayMs, args.pageDelayState.pagesCompleted);
@@ -8915,13 +9149,17 @@ const walkRequest = async (args) => {
8915
9149
  objectsUpserted: upserted,
8916
9150
  creditsSpent: args.credits.spent
8917
9151
  };
8918
- const response = await (async () => {
8919
- try {
8920
- return await args.deps.api.get(path, { headers: args.deps.accountHeaders });
8921
- } catch (error) {
8922
- return wrapPageError(pageContext, error);
8923
- }
8924
- })();
9152
+ let response;
9153
+ while (true) try {
9154
+ response = await args.deps.api.get(path, pageRequestOptions);
9155
+ break;
9156
+ } catch (error) {
9157
+ if (await waitForRateLimit(error)) continue;
9158
+ return wrapPageError({
9159
+ ...pageContext,
9160
+ cursorPersisted: persistPartialCursor(error) || void 0
9161
+ }, error);
9162
+ }
8925
9163
  const credits = creditsFromHeader(response.headers);
8926
9164
  args.credits.spent += credits;
8927
9165
  let json;
@@ -8989,6 +9227,11 @@ const runSync = async (deps, opts) => {
8989
9227
  let stoppedReason = "complete";
8990
9228
  const pendingCursors = [];
8991
9229
  const pageDelayState = { pagesCompleted: 0 };
9230
+ const rateLimitNow = opts.rateLimit?.now ?? Date.now;
9231
+ const rateLimitState = opts.rateLimit ? {
9232
+ deadlineMs: opts.rateLimit.timeoutSeconds === void 0 ? void 0 : rateLimitNow() + opts.rateLimit.timeoutSeconds * 1e3,
9233
+ retryCount: 0
9234
+ } : void 0;
8992
9235
  const addResult = (result) => {
8993
9236
  pages += result.pages;
8994
9237
  upserted += result.upserted;
@@ -9003,16 +9246,21 @@ const runSync = async (deps, opts) => {
9003
9246
  const parentIds = [];
9004
9247
  const parentPersistsCheckpoint = parentCollection.requests.length === 1;
9005
9248
  let parentCheckpoint;
9006
- for (const request of parentCollection.requests) {
9249
+ for (const [requestIndex, request] of parentCollection.requests.entries()) {
9007
9250
  const result = await walkRequest({
9008
9251
  deps,
9009
9252
  collection: parentCollection,
9010
9253
  request,
9254
+ requestIndex,
9011
9255
  since: opts.since,
9012
9256
  useCheckpoint: false,
9013
9257
  collectIds: true,
9258
+ allowPartialResume: false,
9014
9259
  credits,
9015
9260
  pageDelayState,
9261
+ rateLimit: opts.rateLimit,
9262
+ rateLimitState,
9263
+ writePendingCursor,
9016
9264
  onPage: opts.onPage,
9017
9265
  onProgress: opts.onProgress
9018
9266
  });
@@ -9030,16 +9278,21 @@ const runSync = async (deps, opts) => {
9030
9278
  lastSynced: Date.now()
9031
9279
  };
9032
9280
  if (parentCheckpoint) writePendingCursor(parentCheckpoint);
9033
- for (const parentId of parentIds) for (const request of collection.requests) addResult(await walkRequest({
9281
+ for (const parentId of parentIds) for (const [requestIndex, request] of collection.requests.entries()) addResult(await walkRequest({
9034
9282
  deps,
9035
9283
  collection,
9036
9284
  request,
9285
+ requestIndex,
9037
9286
  since: opts.since,
9038
9287
  useCheckpoint: false,
9288
+ allowPartialResume: false,
9039
9289
  parentId,
9040
9290
  parentIdToken: collection.parentIdToken,
9041
9291
  credits,
9042
9292
  pageDelayState,
9293
+ rateLimit: opts.rateLimit,
9294
+ rateLimitState,
9295
+ writePendingCursor,
9043
9296
  onPage: opts.onPage,
9044
9297
  onProgress: opts.onProgress
9045
9298
  }));
@@ -9051,15 +9304,24 @@ const runSync = async (deps, opts) => {
9051
9304
  });
9052
9305
  } else {
9053
9306
  const useCheckpoint = collection.requests.length === 1;
9054
- for (const request of collection.requests) {
9307
+ const partialCursor = decodePartialSyncCursor(deps.cache.getCursor(collection.key)?.cursor ?? null);
9308
+ const activePartialCursor = partialCursor && partialCursor.since === (opts.since ?? null) && partialCursor.requestIndex < collection.requests.length ? partialCursor : void 0;
9309
+ for (const [requestIndex, request] of collection.requests.entries()) {
9310
+ if (activePartialCursor && requestIndex < activePartialCursor.requestIndex) continue;
9055
9311
  const result = await walkRequest({
9056
9312
  deps,
9057
9313
  collection,
9058
9314
  request,
9315
+ requestIndex,
9059
9316
  since: opts.since,
9060
9317
  useCheckpoint,
9318
+ resumeCursor: activePartialCursor?.requestIndex === requestIndex ? activePartialCursor : void 0,
9319
+ allowPartialResume: true,
9061
9320
  credits,
9062
9321
  pageDelayState,
9322
+ rateLimit: opts.rateLimit,
9323
+ rateLimitState,
9324
+ writePendingCursor,
9063
9325
  onPage: opts.onPage,
9064
9326
  onProgress: opts.onProgress
9065
9327
  });
@@ -9870,7 +10132,7 @@ pgTable("profiles", {
9870
10132
  id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
9871
10133
  platform: text("platform", { enum: ["linkedin", "x"] }).notNull(),
9872
10134
  providerId: text("provider_id").notNull(),
9873
- handle: text("handle"),
10135
+ username: text("username"),
9874
10136
  displayName: text("display_name").notNull(),
9875
10137
  avatarURL: text("avatar_url"),
9876
10138
  headline: text("headline"),
@@ -9879,7 +10141,7 @@ pgTable("profiles", {
9879
10141
  updatedAt: timestamptz("updated_at").notNull().defaultNow()
9880
10142
  }, (table) => [
9881
10143
  uniqueIndex("profiles_platform_provider_id_pkey").on(table.platform, table.providerId),
9882
- index("profiles_platform_handle_idx").on(table.platform, table.handle),
10144
+ index("profiles_platform_username_idx").on(table.platform, table.username),
9883
10145
  check("profiles_platform_check", sql`${table.platform} in ('linkedin', 'x')`)
9884
10146
  ]);
9885
10147
  pgTable("feedback_submissions", {
@@ -9902,7 +10164,7 @@ pgTable("wrapped_runs", {
9902
10164
  userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
9903
10165
  connectedAccountId: text("connected_account_id").references(() => connectedAccounts.id, { onDelete: "set null" }),
9904
10166
  platform: text("platform", { enum: ["x"] }).notNull().default("x"),
9905
- handle: text("handle").notNull(),
10167
+ username: text("username").notNull(),
9906
10168
  platformProfileId: text("platform_profile_id"),
9907
10169
  status: text("status", { enum: [
9908
10170
  "pending",
@@ -9922,7 +10184,7 @@ pgTable("wrapped_runs", {
9922
10184
  }, (table) => [
9923
10185
  uniqueIndex("wrapped_runs_share_slug_key").on(table.shareSlug),
9924
10186
  index("wrapped_runs_user_created_idx").on(table.userId, table.createdAt.desc()),
9925
- index("wrapped_runs_handle_done_idx").on(table.handle, table.createdAt.desc()).where(sql`${table.status} = 'done'`),
10187
+ index("wrapped_runs_username_done_idx").on(table.username, table.createdAt.desc()).where(sql`${table.status} = 'done'`),
9926
10188
  check("wrapped_runs_platform_check", sql`${table.platform} in ('x')`),
9927
10189
  check("wrapped_runs_status_check", sql`${table.status} in ('pending', 'running', 'done', 'failed')`)
9928
10190
  ]);
@@ -9987,16 +10249,16 @@ const createURL = (baseURL, path) => {
9987
10249
  const createAbsoluteURL = (baseURL, path) => createURL(baseURL, path).toString();
9988
10250
  //#endregion
9989
10251
  //#region ../../packages/linkedin/src/profiles.ts
9990
- const displayNameFrom = (firstName, lastName, handle) => [firstName, lastName].filter(Boolean).join(" ").trim() || handle || "";
10252
+ const displayNameFrom = (firstName, lastName, username) => [firstName, lastName].filter(Boolean).join(" ").trim() || username || "";
9991
10253
  const rowFrom$1 = (fields) => {
9992
10254
  const providerId = fields.providerId?.trim();
9993
10255
  if (!providerId) return null;
9994
- const handle = fields.handle?.trim() || null;
9995
- const displayName = fields.displayName?.trim() || displayNameFrom(fields.firstName, fields.lastName, handle ?? void 0);
10256
+ const username = fields.username?.trim() || null;
10257
+ const displayName = fields.displayName?.trim() || displayNameFrom(fields.firstName, fields.lastName, username ?? void 0);
9996
10258
  if (!displayName) return null;
9997
10259
  return {
9998
10260
  providerId,
9999
- handle,
10261
+ username,
10000
10262
  displayName,
10001
10263
  avatarURL: fields.avatarURL?.trim() || null,
10002
10264
  headline: fields.headline?.trim() || null,
@@ -10028,7 +10290,7 @@ const rowFromUser = ({ user, providerId, raw }) => rowFrom$1({
10028
10290
  displayName: user.display_name,
10029
10291
  firstName: user.first_name,
10030
10292
  lastName: user.last_name,
10031
- handle: user.public_identifier,
10293
+ username: user.public_identifier,
10032
10294
  avatarURL: user.public_picture_url,
10033
10295
  headline: headlineFromUser(user),
10034
10296
  raw
@@ -10042,7 +10304,7 @@ const extractProfile$1 = (body) => {
10042
10304
  displayName: parsed.data.display_name,
10043
10305
  firstName: parsed.data.first_name,
10044
10306
  lastName: parsed.data.last_name,
10045
- handle: parsed.data.public_identifier,
10307
+ username: parsed.data.public_identifier,
10046
10308
  avatarURL: parsed.data.public_picture_url,
10047
10309
  headline: headlineFromUser(parsed.data),
10048
10310
  raw: parsed.data
@@ -13227,7 +13489,7 @@ const buildRuntimeArgs = (adapter, op) => {
13227
13489
  };
13228
13490
  args.account = {
13229
13491
  type: "string",
13230
- description: "Account selector: @handle or profile_id:<id>; overrides config default"
13492
+ description: "Account selector: @username or profile_id:<id>; overrides config default"
13231
13493
  };
13232
13494
  if (adapter.isCacheable(op.endpoint)) args.header = {
13233
13495
  type: "string",
@@ -14150,7 +14412,7 @@ const linkedinRuntimeAdapter = {
14150
14412
  accountFromOwnAccount: (ownAccount) => ownAccount.account,
14151
14413
  ownAccountTargetId: (ownAccount) => ownAccount.providerId,
14152
14414
  ownAccountHeader: (ownAccount, selectedAccount) => ownAccount?.accountIdHeader ?? selectedAccount.selector,
14153
- accountRecordInput: (ownAccount) => ownAccount.account.handle ?? ownAccount.providerId,
14415
+ accountRecordInput: (ownAccount) => ownAccount.account.username ?? ownAccount.providerId,
14154
14416
  argAliases: argNameAliasesFrom,
14155
14417
  bodyParamArg,
14156
14418
  bodyValidationError: ({ bodyFromJSON, bodyParamEntries, bodyParamValues, bodyText, bodyTextArg, displayTextArg, op }) => {
@@ -14254,6 +14516,8 @@ const DM_INTERACTION_ENDPOINT_BILLING = {
14254
14516
  billingCategory: DM_INTERACTION_BILLING_CATEGORY,
14255
14517
  costBPS: costBPSForCategory$1(DM_INTERACTION_BILLING_CATEGORY)
14256
14518
  };
14519
+ const LINKEDIN_RATE_LIMIT_RETRY_LIMIT$1 = 25;
14520
+ const LINKEDIN_RATE_LIMIT_MAX_FALLBACK_SECONDS$1 = 60;
14257
14521
  const linkedinSyncPageDelayMs = (pagesCompleted) => {
14258
14522
  if (pagesCompleted < 10) return 1e3;
14259
14523
  if (pagesCompleted < 20) return 2500;
@@ -14346,6 +14610,60 @@ const createSyncProgressReporter = (deps, collectionKey) => {
14346
14610
  stop: () => void 0
14347
14611
  };
14348
14612
  };
14613
+ const writeRateLimitRetryNotice$1 = (args) => {
14614
+ process.stderr.write(`LinkedIn rate limited; waiting ${args.delaySeconds}s before retry ${args.retryCount}/${LINKEDIN_RATE_LIMIT_RETRY_LIMIT$1}.\n`);
14615
+ };
14616
+ const linkedInRateLimitFallbackDelaySeconds$1 = (retryCount) => Math.min(2 ** Math.max(0, retryCount - 1), LINKEDIN_RATE_LIMIT_MAX_FALLBACK_SECONDS$1);
14617
+ const shellQuote$1 = (value) => /^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
14618
+ const syncRetryCommand$1 = (platform, args) => {
14619
+ const parts = [
14620
+ "social",
14621
+ platform,
14622
+ "sync"
14623
+ ];
14624
+ if (args.collection) parts.push(String(args.collection));
14625
+ if (typeof args.account === "string") parts.push("--account", args.account);
14626
+ if (typeof args.since === "string") parts.push("--since", args.since);
14627
+ if (args.timeout != null) parts.push("--timeout", String(args.timeout));
14628
+ return parts.map(shellQuote$1).join(" ");
14629
+ };
14630
+ const rateLimitCauseFor$1 = (error) => {
14631
+ if (isRateLimitError(error)) return error;
14632
+ if (error instanceof SyncPageError && isRateLimitError(error.cause)) return error.cause;
14633
+ };
14634
+ const enrichRateLimitSyncError$1 = (args) => {
14635
+ const cause = rateLimitCauseFor$1(args.error);
14636
+ if (!cause || typeof cause !== "object") return;
14637
+ const retryAfterSeconds = retryAfterSecondsFromRateLimitError(cause);
14638
+ const resumeAt = retryAfterSeconds === void 0 ? void 0 : new Date(Date.now() + retryAfterSeconds * 1e3).toISOString();
14639
+ const retryCommand = syncRetryCommand$1(args.platform, args.commandArgs);
14640
+ const cursorPersisted = args.error instanceof SyncPageError && args.error.cursorPersisted === true;
14641
+ const savedPages = args.error instanceof SyncPageError && args.error.pagesCompleted > 0;
14642
+ const retryAt = resumeAt ? ` at or after ${resumeAt}` : "";
14643
+ Object.assign(cause, {
14644
+ resumeAt,
14645
+ retryCommand,
14646
+ hint: `Rate limited. Wait${retryAt}, then run \`${retryCommand}\`. ${cursorPersisted ? "Already-synced pages are saved; this sync will resume from the saved cursor." : savedPages ? "Already-synced rows are saved, but this collection cannot safely resume the partial page walk; the re-run may re-fetch unfinished pages." : "No sync page completed before the abort."}`,
14647
+ syncResume: {
14648
+ collection: args.collectionKey,
14649
+ command: retryCommand,
14650
+ cursorPersisted,
14651
+ savedPages,
14652
+ behavior: cursorPersisted ? "resume" : "rerun"
14653
+ }
14654
+ });
14655
+ return cause;
14656
+ };
14657
+ const syncRateLimitOptions$1 = (timeoutSeconds) => ({
14658
+ retryAfterSecondsFor: retryAfterSecondsFromRateLimitError,
14659
+ fallbackDelaySecondsForRetry: linkedInRateLimitFallbackDelaySeconds$1,
14660
+ isRateLimitError,
14661
+ ...timeoutSeconds === void 0 ? {} : {
14662
+ timeoutSeconds,
14663
+ maxRetries: LINKEDIN_RATE_LIMIT_RETRY_LIMIT$1,
14664
+ onRetry: writeRateLimitRetryNotice$1
14665
+ }
14666
+ });
14349
14667
  const buildSync$1 = (deps, platform) => defineCommand({
14350
14668
  meta: commandMeta$2({
14351
14669
  name: "sync",
@@ -14401,7 +14719,7 @@ const buildSync$1 = (deps, platform) => defineCommand({
14401
14719
  },
14402
14720
  since: {
14403
14721
  type: "string",
14404
- description: "ISO date/datetime or compact duration like 2:days|3:weeks|5:hours"
14722
+ description: "ISO date (2026-05-04) or datetime (2026-05-04T00:00:00Z)"
14405
14723
  },
14406
14724
  reset: {
14407
14725
  type: "boolean",
@@ -14410,6 +14728,10 @@ const buildSync$1 = (deps, platform) => defineCommand({
14410
14728
  account: {
14411
14729
  type: "string",
14412
14730
  description: "Account selector; defaults to the first connected account"
14731
+ },
14732
+ timeout: {
14733
+ type: "string",
14734
+ description: "Optional LinkedIn rate-limit wait budget in seconds"
14413
14735
  }
14414
14736
  },
14415
14737
  run: async ({ args }) => {
@@ -14421,6 +14743,7 @@ const buildSync$1 = (deps, platform) => defineCommand({
14421
14743
  const accountSelector = requireAccountSelector$1(res.account.selector);
14422
14744
  const accountHeaders = { "x-social-account-id": accountSelector };
14423
14745
  if (args.credits != null) throw new UsageError("Unsupported option --credits for linkedin sync.");
14746
+ const timeoutSeconds = args.timeout == null ? void 0 : deps.parseIntegerString("--timeout", args.timeout, { min: 1 });
14424
14747
  if (!args.collection) {
14425
14748
  if (args.reset === true) throw new UsageError("--reset requires a collection");
14426
14749
  const cache = openLinkedinCache({
@@ -14493,12 +14816,20 @@ const buildSync$1 = (deps, platform) => defineCommand({
14493
14816
  collection,
14494
14817
  since,
14495
14818
  onPage: progress.onPage,
14496
- onProgress
14819
+ onProgress,
14820
+ rateLimit: syncRateLimitOptions$1(timeoutSeconds)
14497
14821
  });
14498
14822
  progress.stop(summary);
14499
14823
  deps.printData(summary);
14500
14824
  } catch (error) {
14501
14825
  progress.error();
14826
+ const rateLimitError = enrichRateLimitSyncError$1({
14827
+ error,
14828
+ platform,
14829
+ commandArgs: args,
14830
+ collectionKey: collection.key
14831
+ });
14832
+ if (rateLimitError) throw rateLimitError;
14502
14833
  throw error;
14503
14834
  } finally {
14504
14835
  cache.close();
@@ -14532,7 +14863,7 @@ const buildSql$1 = (deps, platform) => defineCommand({
14532
14863
  formats: ["json"],
14533
14864
  projection: false
14534
14865
  },
14535
- examples: [{ command: "social linkedin sql \"SELECT * FROM li_messages LIMIT 5\"" }],
14866
+ examples: [{ command: "social linkedin sql \"SELECT user_username, user_name, user_headline FROM li_connections WHERE user_headline LIKE '%founder%' LIMIT 20\"" }, { command: "social linkedin sql \"SELECT sender_name, text FROM li_messages ORDER BY created_at DESC LIMIT 20\"" }],
14536
14867
  cost: {
14537
14868
  metered: false,
14538
14869
  unit: "local_cache",
@@ -14659,7 +14990,7 @@ const buildMark = (deps) => {
14659
14990
  },
14660
14991
  account: {
14661
14992
  type: "string",
14662
- description: "Account selector: @handle or profile_id:<id>; overrides config default"
14993
+ description: "Account selector: @username or profile_id:<id>; overrides config default"
14663
14994
  }
14664
14995
  },
14665
14996
  run: async ({ args, cmd }) => {
@@ -14770,7 +15101,7 @@ const buildMessageDelete = (deps) => {
14770
15101
  },
14771
15102
  account: {
14772
15103
  type: "string",
14773
- description: "Account selector: @handle or profile_id:<id>; overrides config default"
15104
+ description: "Account selector: @username or profile_id:<id>; overrides config default"
14774
15105
  }
14775
15106
  },
14776
15107
  run: async ({ args, cmd }) => {
@@ -14861,7 +15192,7 @@ const buildMessageEdit = (deps) => {
14861
15192
  },
14862
15193
  account: {
14863
15194
  type: "string",
14864
- description: "Account selector: @handle or profile_id:<id>; overrides config default"
15195
+ description: "Account selector: @username or profile_id:<id>; overrides config default"
14865
15196
  }
14866
15197
  },
14867
15198
  run: async ({ args, cmd }) => {
@@ -14925,12 +15256,12 @@ const buildMessage$1 = (deps, sendCommand) => {
14925
15256
  target: {
14926
15257
  type: "positional",
14927
15258
  required: true,
14928
- description: "LinkedIn person or conversation target: @handle, profile URL, profile_id:<id>, chat_id:<id>, or a messaging thread URL",
15259
+ description: "LinkedIn person or conversation target: @username, profile URL, profile_id:<id>, chat_id:<id>, or a messaging thread URL",
14929
15260
  valueHint: "target"
14930
15261
  },
14931
15262
  account: {
14932
15263
  type: "string",
14933
- description: "Account selector: @handle or profile_id:<id>; overrides config default"
15264
+ description: "Account selector: @username or profile_id:<id>; overrides config default"
14934
15265
  }
14935
15266
  },
14936
15267
  run: async ({ args }) => {
@@ -15014,7 +15345,7 @@ const buildPosts = (liveCommand) => defineCommand({
15014
15345
  });
15015
15346
  }
15016
15347
  });
15017
- const messagesTeachingUsage = "LinkedIn messages live in your local mirror. Read them with: social linkedin sql \"SELECT * FROM li_messages ORDER BY timestamp DESC LIMIT 20\" (after `social linkedin sync messages`). To change read state: social linkedin messages chat_id:<id> mark read|unread";
15348
+ const messagesTeachingUsage = "LinkedIn messages live in your local mirror. Read them with: social linkedin sql \"SELECT sender_name, text FROM li_messages ORDER BY created_at DESC LIMIT 20\" (after `social linkedin sync messages`). To change read state: social linkedin messages chat_id:<id> mark read|unread";
15018
15349
  const buildMessages = (deps) => {
15019
15350
  const markCommand = buildMark(deps);
15020
15351
  const schemaContract = markCommand.meta.schemaContract;
@@ -15037,7 +15368,7 @@ const buildMessages = (deps) => {
15037
15368
  },
15038
15369
  account: {
15039
15370
  type: "string",
15040
- description: "Account selector: @handle or profile_id:<id>; overrides config default"
15371
+ description: "Account selector: @username or profile_id:<id>; overrides config default"
15041
15372
  }
15042
15373
  },
15043
15374
  run: async ({ args }) => {
@@ -15374,12 +15705,12 @@ const XUser = looseObject({
15374
15705
  const rowFrom = (user) => {
15375
15706
  const providerId = user.id.trim();
15376
15707
  if (!providerId) return null;
15377
- const handle = user.username?.trim() || null;
15378
- const displayName = user.name?.trim() || handle || "";
15708
+ const username = user.username?.trim() || null;
15709
+ const displayName = user.name?.trim() || username || "";
15379
15710
  if (!displayName) return null;
15380
15711
  return {
15381
15712
  providerId,
15382
- handle,
15713
+ username,
15383
15714
  displayName,
15384
15715
  avatarURL: user.profile_image_url?.trim() || null,
15385
15716
  headline: user.description?.trim() || null,
@@ -15607,7 +15938,7 @@ const X_OPERATIONS = [
15607
15938
  required: true,
15608
15939
  kind: "string",
15609
15940
  valueHint: "target",
15610
- description: "Target profile: @handle, profile URL, or profile_id:<id>"
15941
+ description: "Target profile: @username, profile URL, or profile_id:<id>"
15611
15942
  }]
15612
15943
  },
15613
15944
  {
@@ -16159,7 +16490,7 @@ const X_OPERATIONS = [
16159
16490
  required: true,
16160
16491
  kind: "string",
16161
16492
  valueHint: "target",
16162
- description: "Target profile: @handle, profile URL, or profile_id:<id>. Defaults to the selected account when omitted."
16493
+ description: "Target profile: @username, profile URL, or profile_id:<id>. Defaults to the selected account when omitted."
16163
16494
  },
16164
16495
  {
16165
16496
  name: "user.fields",
@@ -16335,7 +16666,7 @@ const X_OPERATIONS = [
16335
16666
  required: true,
16336
16667
  kind: "string",
16337
16668
  valueHint: "target",
16338
- description: "Target profile: @handle, profile URL, or profile_id:<id>. Defaults to the selected account when omitted."
16669
+ description: "Target profile: @username, profile URL, or profile_id:<id>. Defaults to the selected account when omitted."
16339
16670
  },
16340
16671
  {
16341
16672
  name: "max_results",
@@ -16419,7 +16750,7 @@ const X_OPERATIONS = [
16419
16750
  required: true,
16420
16751
  kind: "string",
16421
16752
  valueHint: "target",
16422
- description: "Target profile: @handle, profile URL, or profile_id:<id>. Defaults to the selected account when omitted."
16753
+ description: "Target profile: @username, profile URL, or profile_id:<id>. Defaults to the selected account when omitted."
16423
16754
  },
16424
16755
  {
16425
16756
  name: "max_results",
@@ -16510,7 +16841,7 @@ const X_OPERATIONS = [
16510
16841
  required: true,
16511
16842
  kind: "string",
16512
16843
  valueHint: "target",
16513
- description: "Target profile: @handle, profile URL, or profile_id:<id>. Defaults to the selected account when omitted."
16844
+ description: "Target profile: @username, profile URL, or profile_id:<id>. Defaults to the selected account when omitted."
16514
16845
  },
16515
16846
  {
16516
16847
  name: "max_results",
@@ -16622,7 +16953,7 @@ const X_OPERATIONS = [
16622
16953
  required: true,
16623
16954
  kind: "string",
16624
16955
  valueHint: "target",
16625
- description: "Target profile: @handle, profile URL, or profile_id:<id>. Defaults to the selected account when omitted."
16956
+ description: "Target profile: @username, profile URL, or profile_id:<id>. Defaults to the selected account when omitted."
16626
16957
  },
16627
16958
  {
16628
16959
  name: "since_id",
@@ -16905,7 +17236,7 @@ const X_OPERATIONS = [
16905
17236
  required: true,
16906
17237
  kind: "string",
16907
17238
  valueHint: "target",
16908
- description: "Target profile: @handle, profile URL, or profile_id:<id>. Defaults to the selected account when omitted."
17239
+ description: "Target profile: @username, profile URL, or profile_id:<id>. Defaults to the selected account when omitted."
16909
17240
  },
16910
17241
  {
16911
17242
  name: "since_id",
@@ -17909,13 +18240,13 @@ const targetDescriptionFor = (targetKind) => {
17909
18240
  const kinds = kindsOf(targetKind);
17910
18241
  if (kinds.length === 1) {
17911
18242
  const [kind] = kinds;
17912
- if (kind === "profile") return "Target profile: @handle, profile URL, or profile_id:<id>";
18243
+ if (kind === "profile") return "Target profile: @username, profile URL, or profile_id:<id>";
17913
18244
  if (kind === "post") return "Target post: a post URL or post_id:<id>";
17914
18245
  if (kind === "list") return "Target list: a list URL or list_id:<id>";
17915
18246
  if (kind === "chat") return "Target conversation: a chat URL or chat_id:<id>";
17916
18247
  return "Target company: a company URL or company_id:<id>";
17917
18248
  }
17918
- return "Target: @handle, URL, or a typed ID such as profile_id:<id>.";
18249
+ return "Target: @username, URL, or a typed ID such as profile_id:<id>.";
17919
18250
  };
17920
18251
  const commandParameters = (op) => op.parameters.filter((p) => !(p.in === "path" && ownMode(op, p.name) === "implicit"));
17921
18252
  const parameterHelpRank = (op, param, index) => {
@@ -18162,7 +18493,7 @@ const xRuntimeAdapter = {
18162
18493
  accountFromOwnAccount: (ownAccount) => ownAccount.account,
18163
18494
  ownAccountTargetId: (ownAccount) => ownAccount.xUserId,
18164
18495
  ownAccountHeader: (_ownAccount, selectedAccount) => selectedAccount.selector,
18165
- accountRecordInput: (ownAccount) => ownAccount.account.handle ?? ownAccount.xUserId,
18496
+ accountRecordInput: (ownAccount) => ownAccount.account.username ?? ownAccount.xUserId,
18166
18497
  argAliases,
18167
18498
  bodyTextResolvesTarget: resolvesTargetArg,
18168
18499
  bodyTextTargetRequired: resolvesTargetArg,
@@ -18238,6 +18569,50 @@ const syncListRowFor = (collection, shortName, state) => {
18238
18569
  objectCount: lastSynced === null ? null : state?.objectCount ?? 0
18239
18570
  };
18240
18571
  };
18572
+ const shellQuote = (value) => /^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
18573
+ const syncRetryCommand = (platform, args) => {
18574
+ const parts = [
18575
+ "social",
18576
+ platform,
18577
+ "sync"
18578
+ ];
18579
+ if (args.collection) parts.push(String(args.collection));
18580
+ if (typeof args.account === "string") parts.push("--account", args.account);
18581
+ if (typeof args.since === "string") parts.push("--since", args.since);
18582
+ if (args.timeout != null) parts.push("--timeout", String(args.timeout));
18583
+ return parts.map(shellQuote).join(" ");
18584
+ };
18585
+ const rateLimitCauseFor = (error) => {
18586
+ if (isRateLimitError(error)) return error;
18587
+ if (error instanceof SyncPageError && isRateLimitError(error.cause)) return error.cause;
18588
+ };
18589
+ const enrichRateLimitSyncError = (args) => {
18590
+ const cause = rateLimitCauseFor(args.error);
18591
+ if (!cause || typeof cause !== "object") return;
18592
+ const retryAfterSeconds = retryAfterSecondsFromRateLimitError(cause);
18593
+ const resumeAt = retryAfterSeconds === void 0 ? void 0 : new Date(Date.now() + retryAfterSeconds * 1e3).toISOString();
18594
+ const retryCommand = syncRetryCommand(args.platform, args.commandArgs);
18595
+ const cursorPersisted = args.error instanceof SyncPageError && args.error.cursorPersisted === true;
18596
+ const savedPages = args.error instanceof SyncPageError && args.error.pagesCompleted > 0;
18597
+ const retryAt = resumeAt ? ` at or after ${resumeAt}` : "";
18598
+ Object.assign(cause, {
18599
+ resumeAt,
18600
+ retryCommand,
18601
+ hint: `Rate limited. Wait${retryAt}, then run \`${retryCommand}\`. ${cursorPersisted ? "Already-synced pages are saved; this sync will resume from the saved cursor." : savedPages ? "Already-synced rows are saved, but this collection cannot safely resume the partial page walk; the re-run may re-fetch unfinished pages." : "No sync page completed before the abort."}`,
18602
+ syncResume: {
18603
+ collection: args.collectionKey,
18604
+ command: retryCommand,
18605
+ cursorPersisted,
18606
+ savedPages,
18607
+ behavior: cursorPersisted ? "resume" : "rerun"
18608
+ }
18609
+ });
18610
+ return cause;
18611
+ };
18612
+ const syncRateLimitOptions = () => ({
18613
+ retryAfterSecondsFor: retryAfterSecondsFromRateLimitError,
18614
+ isRateLimitError
18615
+ });
18241
18616
  const buildSync = (deps, platform) => defineCommand({
18242
18617
  meta: commandMeta$1({
18243
18618
  name: "sync",
@@ -18288,7 +18663,7 @@ const buildSync = (deps, platform) => defineCommand({
18288
18663
  },
18289
18664
  since: {
18290
18665
  type: "string",
18291
- description: "ISO date/datetime or compact duration like 2:days|3:weeks|5:hours"
18666
+ description: "ISO date (2026-05-04) or datetime (2026-05-04T00:00:00Z)"
18292
18667
  },
18293
18668
  reset: {
18294
18669
  type: "boolean",
@@ -18297,6 +18672,10 @@ const buildSync = (deps, platform) => defineCommand({
18297
18672
  account: {
18298
18673
  type: "string",
18299
18674
  description: "Account selector; defaults to the first connected account"
18675
+ },
18676
+ timeout: {
18677
+ type: "string",
18678
+ description: "Optional rate-limit wait budget in seconds"
18300
18679
  }
18301
18680
  },
18302
18681
  run: async ({ args }) => {
@@ -18308,6 +18687,7 @@ const buildSync = (deps, platform) => defineCommand({
18308
18687
  const accountSelector = requireAccountSelector(res.account.selector);
18309
18688
  const accountHeaders = { "x-social-account-id": accountSelector };
18310
18689
  if (args.credits != null) throw new UsageError("Unsupported option --credits for x sync.");
18690
+ if (args.timeout != null) deps.parseIntegerString("--timeout", args.timeout, { min: 1 });
18311
18691
  if (!args.collection) {
18312
18692
  if (args.reset === true) throw new UsageError("--reset requires a collection");
18313
18693
  const cache = openXCache({
@@ -18366,9 +18746,19 @@ const buildSync = (deps, platform) => defineCommand({
18366
18746
  }, {
18367
18747
  collection,
18368
18748
  since,
18369
- onProgress
18749
+ onProgress,
18750
+ rateLimit: syncRateLimitOptions()
18370
18751
  });
18371
18752
  deps.printData(summary);
18753
+ } catch (error) {
18754
+ const rateLimitError = enrichRateLimitSyncError({
18755
+ error,
18756
+ platform,
18757
+ commandArgs: args,
18758
+ collectionKey: collection.key
18759
+ });
18760
+ if (rateLimitError) throw rateLimitError;
18761
+ throw error;
18372
18762
  } finally {
18373
18763
  cache.close();
18374
18764
  }
@@ -18403,7 +18793,7 @@ const buildSql = (deps, platform) => {
18403
18793
  formats: ["json"],
18404
18794
  projection: false
18405
18795
  },
18406
- examples: [{ command: "social x sql \"SELECT * FROM x_tweets LIMIT 5\"" }],
18796
+ examples: [{ command: "social x sql \"SELECT username, name, followers_count FROM x_followers ORDER BY followers_count DESC LIMIT 20\"" }, { command: "social x sql \"SELECT username, name FROM x_following WHERE verified = 1 ORDER BY followers_count DESC LIMIT 20\"" }],
18407
18797
  cost: {
18408
18798
  metered: false,
18409
18799
  unit: "local_cache",
@@ -18509,7 +18899,7 @@ const buildTweets = (deps) => {
18509
18899
  if (args.target !== void 0) args.target = {
18510
18900
  ...args.target,
18511
18901
  required: true,
18512
- description: "Target profile: @handle, profile URL, or profile_id:<id>"
18902
+ description: "Target profile: @username, profile URL, or profile_id:<id>"
18513
18903
  };
18514
18904
  return defineCommand({
18515
18905
  meta: commandMeta$1({
@@ -18543,12 +18933,12 @@ const buildMessage = (deps) => {
18543
18933
  recipients: {
18544
18934
  type: "positional",
18545
18935
  required: true,
18546
- description: "Recipient(s): chat URL, chat_id:<id>, @handle, profile URL, or profile_id:<id>; comma-separate profiles for a group",
18936
+ description: "Recipient(s): chat URL, chat_id:<id>, @username, profile URL, or profile_id:<id>; comma-separate profiles for a group",
18547
18937
  valueHint: "recipients"
18548
18938
  },
18549
18939
  account: {
18550
18940
  type: "string",
18551
- description: "Account selector: @handle or profile_id:<id>; overrides config default"
18941
+ description: "Account selector: @username or profile_id:<id>; overrides config default"
18552
18942
  }
18553
18943
  },
18554
18944
  run: async ({ args }) => {
@@ -18664,7 +19054,7 @@ const buildReplies = (deps) => {
18664
19054
  },
18665
19055
  account: {
18666
19056
  type: "string",
18667
- description: "Account selector: @handle or profile_id:<id>; overrides config default"
19057
+ description: "Account selector: @username or profile_id:<id>; overrides config default"
18668
19058
  }
18669
19059
  },
18670
19060
  run: async ({ args }) => {
@@ -18834,14 +19224,14 @@ const prepareAccountConnect$1 = async (deps) => {
18834
19224
  await pollForSeat$2(deps);
18835
19225
  };
18836
19226
  const normalizeIdentifier$1 = (value) => value.trim().replace(LEADING_AT_PATTERN$1, "").toLowerCase();
18837
- const accountHandle = (account) => account.handle.replace(LEADING_AT_PATTERN$1, "");
19227
+ const accountUsername = (account) => account.username.replace(LEADING_AT_PATTERN$1, "");
18838
19228
  const linkedinReconnectTargetFrom = (value) => {
18839
19229
  const trimmed = value.trim();
18840
19230
  if (trimmed.startsWith("@")) {
18841
- const handle = normalizeIdentifier$1(trimmed);
18842
- if (handle) return {
18843
- type: "handle",
18844
- value: handle
19231
+ const username = normalizeIdentifier$1(trimmed);
19232
+ if (username) return {
19233
+ type: "username",
19234
+ value: username
18845
19235
  };
18846
19236
  }
18847
19237
  if (trimmed.startsWith("profile_id:")) {
@@ -18856,20 +19246,20 @@ const linkedinReconnectTargetFrom = (value) => {
18856
19246
  if ((url.protocol === "https:" || url.protocol === "http:") && LINKEDIN_PROFILE_HOST_PATTERN.test(url.hostname.replace(/^www\./i, ""))) {
18857
19247
  const [, section, publicIdentifier] = url.pathname.split("/");
18858
19248
  if (section === "in" && publicIdentifier) return {
18859
- type: "handle",
19249
+ type: "username",
18860
19250
  value: decodeURIComponent(publicIdentifier).toLowerCase()
18861
19251
  };
18862
19252
  }
18863
19253
  } catch {}
18864
- throw new Error("LinkedIn reconnect account must be @handle, a LinkedIn profile URL, or profile_id:<id>.");
19254
+ throw new Error("LinkedIn reconnect account must be @username, a LinkedIn profile URL, or profile_id:<id>.");
18865
19255
  };
18866
19256
  const accountMatchesReconnectTarget = (account, target) => {
18867
19257
  if (target.type === "profileId") return account.profileId.toLowerCase() === target.value;
18868
- return normalizeIdentifier$1(account.handle) === target.value;
19258
+ return normalizeIdentifier$1(account.username) === target.value;
18869
19259
  };
18870
19260
  const reconnectCommandTargetFor = (account) => {
18871
- const handle = accountHandle(account);
18872
- return normalizeIdentifier$1(handle) === normalizeIdentifier$1(account.profileId) ? `profile_id:${account.profileId}` : `@${handle}`;
19261
+ const username = accountUsername(account);
19262
+ return normalizeIdentifier$1(username) === normalizeIdentifier$1(account.profileId) ? `profile_id:${account.profileId}` : `@${username}`;
18873
19263
  };
18874
19264
  const listLinkedinLifecycleAccounts = (deps, includeDisconnected = true) => deps.client.listAccounts({
18875
19265
  platform: LINKEDIN_PLATFORM,
@@ -18929,7 +19319,7 @@ const reconnectLinkedinAccount = async (deps, identifier) => {
18929
19319
  return {
18930
19320
  platform: LINKEDIN_PLATFORM,
18931
19321
  status: "connected",
18932
- account: await pollForAccount$1(deps, (candidate) => candidate.profileId === account.profileId && candidate.status === CONNECTED_STATUS, `Timed out waiting for LinkedIn account ${account.handle} to reconnect.`)
19322
+ account: await pollForAccount$1(deps, (candidate) => candidate.profileId === account.profileId && candidate.status === CONNECTED_STATUS, `Timed out waiting for LinkedIn account @${accountUsername(account)} to reconnect.`)
18933
19323
  };
18934
19324
  };
18935
19325
  const disconnectLinkedinAccount = async (deps, identifier) => {
@@ -19004,7 +19394,7 @@ const prepareAccountConnect = async (deps) => {
19004
19394
  };
19005
19395
  const accountKey = (account) => account.profileId;
19006
19396
  const normalizeIdentifier = (value) => value.trim().replace(/^@+/, "").toLowerCase();
19007
- const accountSelectorFor$1 = (account) => account.handle || `profile_id:${account.profileId}`;
19397
+ const accountSelectorFor$1 = (account) => account.username ? `@${account.username.replace(/^@+/, "")}` : `profile_id:${account.profileId}`;
19008
19398
  const assertXAccountSelector = (target) => {
19009
19399
  const trimmed = target.trim();
19010
19400
  if (trimmed.startsWith("profile_id:")) {
@@ -19012,7 +19402,7 @@ const assertXAccountSelector = (target) => {
19012
19402
  if (profileId && !/[/?#%]/.test(profileId)) return;
19013
19403
  }
19014
19404
  if (trimmed.startsWith("@") && !/[/?#%]/.test(trimmed)) return;
19015
- throw new Error("account must be an @handle or profile_id:<id>.");
19405
+ throw new Error("account must be an @username or profile_id:<id>.");
19016
19406
  };
19017
19407
  const findAccount = (accounts, target) => {
19018
19408
  const trimmed = target.trim();
@@ -19020,7 +19410,7 @@ const findAccount = (accounts, target) => {
19020
19410
  const profileId = trimmed.slice(11).trim().toLowerCase();
19021
19411
  return accounts.find((account) => account.profileId.toLowerCase() === profileId);
19022
19412
  }
19023
- if (trimmed.startsWith("@")) return accounts.find((account) => normalizeIdentifier(account.handle) === normalizeIdentifier(target));
19413
+ if (trimmed.startsWith("@")) return accounts.find((account) => normalizeIdentifier(account.username) === normalizeIdentifier(target));
19024
19414
  };
19025
19415
  const listXLifecycleAccounts = (deps, includeDisconnected) => deps.client.accounts.list({
19026
19416
  platform: "x",
@@ -21629,7 +22019,7 @@ const createInstance = (defaults) => {
21629
22019
  const ky = createInstance();
21630
22020
  //#endregion
21631
22021
  //#region package.json
21632
- var version$1 = "0.7.1";
22022
+ var version$1 = "0.8.0";
21633
22023
  //#endregion
21634
22024
  //#region src/lib/env.ts
21635
22025
  const URLWithTrailingSlash = url().transform(ensureTrailingSlash);
@@ -21762,12 +22152,20 @@ var CLIAPIError = class extends Error {
21762
22152
  status;
21763
22153
  body;
21764
22154
  retryAfterSeconds;
21765
- constructor(message, status, body, retryAfterSeconds) {
22155
+ hint;
22156
+ resumeAt;
22157
+ retryCommand;
22158
+ syncResume;
22159
+ constructor(message, status, body, retryAfterSeconds, details = {}) {
21766
22160
  super(message);
21767
22161
  this.name = "CLIAPIError";
21768
22162
  this.status = status;
21769
22163
  this.body = body;
21770
22164
  this.retryAfterSeconds = retryAfterSeconds;
22165
+ this.hint = details.hint;
22166
+ this.resumeAt = details.resumeAt;
22167
+ this.retryCommand = details.retryCommand;
22168
+ this.syncResume = details.syncResume;
21771
22169
  }
21772
22170
  };
21773
22171
  const readBearerToken = async () => {
@@ -21777,8 +22175,12 @@ const readBearerToken = async () => {
21777
22175
  };
21778
22176
  const linkedInRateLimitFallbackDelaySeconds = (retryCount) => Math.min(2 ** Math.max(0, retryCount - 1), LINKEDIN_RATE_LIMIT_MAX_FALLBACK_SECONDS);
21779
22177
  const linkedInRateLimitRetryDelaySeconds = (retryCount, retryAfterSeconds) => retryAfterSeconds ?? linkedInRateLimitFallbackDelaySeconds(retryCount);
22178
+ const linkedInRateLimitWaitBudgetCovers = (args) => args.delaySeconds <= (args.waitBudgetSeconds ?? LINKEDIN_RATE_LIMIT_MAX_FALLBACK_SECONDS);
21780
22179
  const isLinkedInProxyRequest = (request) => new URL(request.url).pathname.includes("/linkedin/");
21781
- const shouldRetryLinkedInRateLimit = (args) => args.error instanceof HTTPError && args.error.response.status === 429 && isLinkedInProxyRequest(args.error.request) && args.retryCount <= LINKEDIN_RATE_LIMIT_RETRY_LIMIT;
22180
+ const shouldRetryLinkedInRateLimit = (args) => args.error instanceof HTTPError && args.error.response.status === 429 && isLinkedInProxyRequest(args.error.request) && args.retryCount <= LINKEDIN_RATE_LIMIT_RETRY_LIMIT && linkedInRateLimitWaitBudgetCovers({
22181
+ delaySeconds: linkedInRateLimitRetryDelaySeconds(args.retryCount, retryAfterSecondsFrom(args.error.response.headers.get("retry-after"))),
22182
+ waitBudgetSeconds: args.waitBudgetSeconds
22183
+ });
21782
22184
  const writeRateLimitRetryNotice = (args) => {
21783
22185
  process.stderr.write(`LinkedIn rate limited; waiting ${args.delaySeconds}s before retry ${args.retryCount}/${LINKEDIN_RATE_LIMIT_RETRY_LIMIT}.\n`);
21784
22186
  };
@@ -21970,26 +22372,9 @@ const authWhoamiOutput = async (client, apiURL = env.SOCIAL_API_URL) => {
21970
22372
  };
21971
22373
  }
21972
22374
  };
21973
- //#endregion
21974
- //#region src/lib/config.ts
21975
- const CACHE_MODE_TTL_SECONDS = {
21976
- live: 900,
21977
- analytical: 1440 * 60,
21978
- historical: 10080 * 60
21979
- };
21980
- const CACHE_MODES = [
21981
- "live",
21982
- "analytical",
21983
- "historical"
21984
- ];
21985
- const MAX_CACHE_TTL_SECONDS = CACHE_MODE_TTL_SECONDS.historical;
21986
- const CacheMode = _enum$1([...CACHE_MODES, "custom"]);
21987
- const ConfigurableCacheMode = _enum$1(CACHE_MODES);
21988
- const CLIConfig = object$1({ cache: object$1({
21989
- mode: CacheMode.default("live"),
21990
- cacheTTLSeconds: number$1().int().min(0).max(MAX_CACHE_TTL_SECONDS).optional()
21991
- }).default({ mode: "live" }) });
21992
- const defaultCLIConfig = () => ({ cache: { mode: "live" } });
22375
+ const MAX_CACHE_TTL_SECONDS = 10080 * 60;
22376
+ const CLIConfig = object$1({ cache: object$1({ cacheTTLSeconds: number$1().int().min(0).max(MAX_CACHE_TTL_SECONDS).default(900) }).default({ cacheTTLSeconds: 900 }) });
22377
+ const defaultCLIConfig = () => ({ cache: { cacheTTLSeconds: 900 } });
21993
22378
  const configPath = () => join(process.env.HOME ?? homedir(), ".social", "config.json");
21994
22379
  const parseCLIConfig = (serialized) => CLIConfig.parse(JSON.parse(serialized));
21995
22380
  const readCLIConfig = async () => {
@@ -22007,27 +22392,15 @@ const writeCLIConfig = async (config) => {
22007
22392
  await writeFile(path, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 384 });
22008
22393
  await chmod(path, 384);
22009
22394
  };
22010
- const cacheTTLSecondsFrom = (config) => config.cache.mode === "custom" ? config.cache.cacheTTLSeconds ?? CACHE_MODE_TTL_SECONDS.live : CACHE_MODE_TTL_SECONDS[config.cache.mode];
22011
- const setCacheMode = async (mode) => {
22012
- const next = {
22013
- ...await readCLIConfig(),
22014
- cache: { mode }
22015
- };
22016
- await writeCLIConfig(next);
22017
- return next;
22018
- };
22019
22395
  const setCacheTTLSeconds = async (cacheTTLSeconds) => {
22020
22396
  const next = {
22021
22397
  ...await readCLIConfig(),
22022
- cache: {
22023
- mode: "custom",
22024
- cacheTTLSeconds
22025
- }
22398
+ cache: { cacheTTLSeconds }
22026
22399
  };
22027
22400
  await writeCLIConfig(next);
22028
22401
  return next;
22029
22402
  };
22030
- const cacheControlHeaderFromConfig = async () => `max-age=${cacheTTLSecondsFrom(await readCLIConfig())}`;
22403
+ const cacheControlHeaderFromConfig = async () => `max-age=${(await readCLIConfig()).cache.cacheTTLSeconds}`;
22031
22404
  //#endregion
22032
22405
  //#region src/lib/output.ts
22033
22406
  const isRecord$3 = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
@@ -22216,7 +22589,7 @@ const resultEnvelope = ({ account, contract, data, local, resolved = [], respons
22216
22589
  return {
22217
22590
  account: {
22218
22591
  platform: account.platform,
22219
- handle: account.handle
22592
+ username: account.username
22220
22593
  },
22221
22594
  ...payload,
22222
22595
  meta: {
@@ -22246,7 +22619,11 @@ const errorPayload = (error) => {
22246
22619
  type: error.name,
22247
22620
  status: error.status,
22248
22621
  body: error.body,
22249
- retryAfterSeconds: error.retryAfterSeconds
22622
+ retryAfterSeconds: error.retryAfterSeconds,
22623
+ resumeAt: error.resumeAt,
22624
+ retryCommand: error.retryCommand,
22625
+ hint: error.hint,
22626
+ syncResume: error.syncResume
22250
22627
  };
22251
22628
  if (error instanceof Error) return {
22252
22629
  error: error.message,
@@ -22270,7 +22647,11 @@ const apiErrorCause = (error) => ({
22270
22647
  error: error.message,
22271
22648
  status: error.status,
22272
22649
  body: error.body,
22273
- retryAfterSeconds: error.retryAfterSeconds
22650
+ retryAfterSeconds: error.retryAfterSeconds,
22651
+ resumeAt: error.resumeAt,
22652
+ retryCommand: error.retryCommand,
22653
+ hint: error.hint,
22654
+ syncResume: error.syncResume
22274
22655
  });
22275
22656
  const errorCause = (error) => {
22276
22657
  if (!("cause" in error)) return {};
@@ -22344,7 +22725,7 @@ const TARGET_PREFIXES = {
22344
22725
  request_id: "request"
22345
22726
  };
22346
22727
  const KIND_HINTS = {
22347
- profile: "a profile (@handle, a profile URL, or profile_id:…)",
22728
+ profile: "a profile (@username, a profile URL, or profile_id:…)",
22348
22729
  post: "a post (a post URL or post_id:…)",
22349
22730
  list: "a list (a list URL or list_id:…)",
22350
22731
  chat: "a conversation (a chat URL or chat_id:…)",
@@ -22456,8 +22837,8 @@ const resolveTarget = async (name, value, options) => {
22456
22837
  const list = url.pathname.match(/^\/i\/lists\/(\d+)/)?.[1];
22457
22838
  if (list) return accept("list", list, { url: url.toString() });
22458
22839
  const segments = url.pathname.split("/").filter(Boolean);
22459
- const handle = segments[0];
22460
- if (segments.length === 1 && handle && !X_RESERVED_SEGMENTS.has(handle.toLowerCase())) return await lookupProfile(handle, url.toString());
22840
+ const username = segments[0];
22841
+ if (segments.length === 1 && username && !X_RESERVED_SEGMENTS.has(username.toLowerCase())) return await lookupProfile(username, url.toString());
22461
22842
  }
22462
22843
  if (platform === "linkedin" && isLinkedinHost(url)) {
22463
22844
  const post = linkedinPostURN(input, url);
@@ -22478,10 +22859,10 @@ const resolveTarget = async (name, value, options) => {
22478
22859
  if (colon > 0) {
22479
22860
  const prefix = input.slice(0, colon);
22480
22861
  const kind = TARGET_PREFIXES[prefix];
22481
- if (!kind) throw validationError([name], `${name}: unknown prefix "${prefix}:". Use ${Object.keys(TARGET_PREFIXES).map((targetPrefix) => `${targetPrefix}:<id>`).join(", ")}, @handle, or a URL.`);
22862
+ if (!kind) throw validationError([name], `${name}: unknown prefix "${prefix}:". Use ${Object.keys(TARGET_PREFIXES).map((targetPrefix) => `${targetPrefix}:<id>`).join(", ")}, @username, or a URL.`);
22482
22863
  return accept(kind, parseResourceId(name, input.slice(colon + 1)));
22483
22864
  }
22484
- throw validationError([name], `${name} must be @handle, a URL, or a typed ID (${Object.keys(TARGET_PREFIXES).map((targetPrefix) => `${targetPrefix}:<id>`).join(", ")}). Bare IDs are not accepted.`);
22865
+ throw validationError([name], `${name} must be @username, a URL, or a typed ID (${Object.keys(TARGET_PREFIXES).map((targetPrefix) => `${targetPrefix}:<id>`).join(", ")}). Bare IDs are not accepted.`);
22485
22866
  };
22486
22867
  const plainResourceRecord = (platform, kind, id, options = {}) => resolutionRecord({
22487
22868
  input: options.input ?? id,
@@ -22550,14 +22931,8 @@ const commandMeta = (meta) => {
22550
22931
  contract
22551
22932
  };
22552
22933
  };
22553
- const CacheStateOutput = object$1({
22554
- mode: string$1(),
22555
- cacheTTLSeconds: number$1().int().nonnegative()
22556
- });
22557
- const cacheStateFrom = (config) => ({
22558
- mode: config.cache.mode,
22559
- cacheTTLSeconds: cacheTTLSecondsFrom(config)
22560
- });
22934
+ const CacheStateOutput = object$1({ cacheTTLSeconds: number$1().int().nonnegative() });
22935
+ const cacheStateFrom = (config) => ({ cacheTTLSeconds: config.cache.cacheTTLSeconds });
22561
22936
  const printCacheState = (config) => {
22562
22937
  printData(cacheStateFrom(config));
22563
22938
  };
@@ -22571,79 +22946,43 @@ const configCommand = defineCommand({
22571
22946
  name: "cache",
22572
22947
  description: "Read or configure proxy cache defaults."
22573
22948
  },
22574
- subCommands: {
22575
- mode: defineCommand({
22576
- meta: commandMeta({
22577
- name: "mode",
22578
- description: "Read or set the default cache mode.",
22579
- contract: {
22580
- capability: "none",
22581
- auth: {
22582
- local: true,
22583
- required: false
22584
- },
22585
- mutates: true,
22586
- outputSchema: CacheStateOutput,
22587
- idempotency: "idempotent",
22588
- confirmation: false,
22589
- responseShaping: { formats: ["json"] },
22590
- hazards: [{
22591
- code: "local_config_write",
22592
- message: "Writes local CLI configuration when mode is provided."
22593
- }]
22594
- }
22595
- }),
22596
- args: { mode: {
22597
- type: "positional",
22598
- required: false,
22599
- description: "Cache mode: live, analytical, or historical."
22600
- } },
22601
- run: async ({ args }) => {
22602
- if (args.mode === void 0) {
22603
- printCacheState(await readCLIConfig());
22604
- return;
22605
- }
22606
- printCacheState(await setCacheMode(ConfigurableCacheMode.parse(String(args.mode))));
22949
+ subCommands: { ttl: defineCommand({
22950
+ meta: commandMeta({
22951
+ name: "ttl",
22952
+ description: "Read or set the default cache TTL in seconds.",
22953
+ contract: {
22954
+ capability: "none",
22955
+ auth: {
22956
+ local: true,
22957
+ required: false
22958
+ },
22959
+ mutates: true,
22960
+ outputSchema: CacheStateOutput,
22961
+ idempotency: "idempotent",
22962
+ confirmation: false,
22963
+ responseShaping: { formats: ["json"] },
22964
+ hazards: [{
22965
+ code: "local_config_write",
22966
+ message: "Writes local CLI configuration when ttl_seconds is provided."
22967
+ }]
22607
22968
  }
22608
22969
  }),
22609
- ttl: defineCommand({
22610
- meta: commandMeta({
22611
- name: "ttl",
22612
- description: "Read or set the default cache TTL in seconds.",
22613
- contract: {
22614
- capability: "none",
22615
- auth: {
22616
- local: true,
22617
- required: false
22618
- },
22619
- mutates: true,
22620
- outputSchema: CacheStateOutput,
22621
- idempotency: "idempotent",
22622
- confirmation: false,
22623
- responseShaping: { formats: ["json"] },
22624
- hazards: [{
22625
- code: "local_config_write",
22626
- message: "Writes local CLI configuration when ttl_seconds is provided."
22627
- }]
22628
- }
22629
- }),
22630
- args: { ttl_seconds: {
22631
- type: "positional",
22632
- required: false,
22633
- description: "Cache TTL in seconds. Setting this switches cache mode to custom."
22634
- } },
22635
- run: async ({ args }) => {
22636
- if (args.ttl_seconds === void 0) {
22637
- printCacheState(await readCLIConfig());
22638
- return;
22639
- }
22640
- printCacheState(await setCacheTTLSeconds(parseIntegerString("--ttl-seconds", args.ttl_seconds, {
22641
- min: 0,
22642
- max: MAX_CACHE_TTL_SECONDS
22643
- })));
22970
+ args: { ttl_seconds: {
22971
+ type: "positional",
22972
+ required: false,
22973
+ description: "Cache TTL in seconds."
22974
+ } },
22975
+ run: async ({ args }) => {
22976
+ if (args.ttl_seconds === void 0) {
22977
+ printCacheState(await readCLIConfig());
22978
+ return;
22644
22979
  }
22645
- })
22646
- }
22980
+ printCacheState(await setCacheTTLSeconds(parseIntegerString("--ttl-seconds", args.ttl_seconds, {
22981
+ min: 0,
22982
+ max: MAX_CACHE_TTL_SECONDS
22983
+ })));
22984
+ }
22985
+ }) }
22647
22986
  }) }
22648
22987
  });
22649
22988
  //#endregion
@@ -28928,7 +29267,7 @@ const writeAuthContract = {
28928
29267
  };
28929
29268
  const LifecycleAccountOutput = object$1({
28930
29269
  platform: string$1(),
28931
- handle: string$1(),
29270
+ username: string$1(),
28932
29271
  profileId: string$1(),
28933
29272
  url: string$1(),
28934
29273
  displayName: string$1(),
@@ -28994,17 +29333,17 @@ const AccountOverviewOutput = object$1({
28994
29333
  const accountArg = {
28995
29334
  type: "positional",
28996
29335
  required: true,
28997
- description: "Account @handle, profile URL, or profile_id:<id>."
29336
+ description: "Account @username, profile URL, or profile_id:<id>."
28998
29337
  };
28999
29338
  const xAccountArg = {
29000
29339
  type: "positional",
29001
29340
  required: true,
29002
- description: "X account @handle or profile_id:<id>."
29341
+ description: "X account @username or profile_id:<id>."
29003
29342
  };
29004
29343
  const linkedinReconnectAccountArg = {
29005
29344
  type: "positional",
29006
29345
  required: true,
29007
- description: "LinkedIn account @handle, profile URL, or profile_id:<id>."
29346
+ description: "LinkedIn account @username, profile URL, or profile_id:<id>."
29008
29347
  };
29009
29348
  const writeOutput = async (deps, value) => {
29010
29349
  if (deps.writeOutput) {
@@ -29030,7 +29369,7 @@ const lifecycleConnectContract = {
29030
29369
  idempotency: "non-idempotent",
29031
29370
  confirmation: false
29032
29371
  };
29033
- const handleFor = (account) => account.handle.startsWith("@") ? account.handle : `@${account.handle}`;
29372
+ const usernameLabelFor = (account) => account.username.startsWith("@") ? account.username : `@${account.username}`;
29034
29373
  const authHandshakeMessage = "Handshaking auth...";
29035
29374
  const lifecycleDepsWithTextLogs = (deps) => ({
29036
29375
  ...deps,
@@ -29055,7 +29394,7 @@ const lifecycleDepsWithTextLogs = (deps) => ({
29055
29394
  }
29056
29395
  });
29057
29396
  const runConnectOutput = async (deps, connect) => {
29058
- writeText(deps, `${handleFor((await connect(lifecycleDepsWithTextLogs(deps))).account)} connected!`);
29397
+ writeText(deps, `${usernameLabelFor((await connect(lifecycleDepsWithTextLogs(deps))).account)} connected!`);
29059
29398
  };
29060
29399
  const createConnectPlatformCommand = (platform, run) => defineCommand({
29061
29400
  meta: commandMeta({
@@ -29263,11 +29602,11 @@ const createFeedbackCommand = (deps) => defineCommand({
29263
29602
  //#region src/lib/account.ts
29264
29603
  const LEADING_AT_PATTERN = /^@+/;
29265
29604
  const normalizeAccountIdentifier = (value) => value.trim().replace(LEADING_AT_PATTERN, "").toLowerCase();
29266
- const accountSelectorFor = (account) => account.handle || `profile_id:${account.profileId}`;
29605
+ const accountSelectorFor = (account) => account.username ? `@${account.username.replace(LEADING_AT_PATTERN, "")}` : `profile_id:${account.profileId}`;
29267
29606
  const accountMatchesIdentifier = (account, identifier) => {
29268
29607
  const trimmed = identifier.trim();
29269
29608
  if (trimmed.startsWith("profile_id:")) return account.profileId.toLowerCase() === trimmed.slice(11).trim().toLowerCase();
29270
- return trimmed.startsWith("@") && normalizeAccountIdentifier(account.handle) === normalizeAccountIdentifier(trimmed);
29609
+ return trimmed.startsWith("@") && normalizeAccountIdentifier(account.username) === normalizeAccountIdentifier(trimmed);
29271
29610
  };
29272
29611
  const resolveAccountId = ({ account }) => {
29273
29612
  if (!account) return;
@@ -29363,12 +29702,12 @@ const createCLIDeps = () => {
29363
29702
  if (account && !selected) throw new NotFoundError("account_not_found");
29364
29703
  return {
29365
29704
  platform,
29366
- handle: selected?.handle ?? (account?.startsWith("@") ? account : `@${account ?? "default"}`),
29705
+ username: selected?.username ?? (account?.startsWith("@") ? account.slice(1) : account ?? "default"),
29367
29706
  selector: selected ? accountSelectorFor(selected) : account
29368
29707
  };
29369
29708
  };
29370
29709
  const lookupXHandle = async (input, account) => {
29371
- const handle = input.replace(/^@+/, "");
29710
+ const inputUsername = input.replace(/^@+/, "");
29372
29711
  const selectedAccount = await resolveAccount({
29373
29712
  account,
29374
29713
  platform: "x"
@@ -29377,13 +29716,13 @@ const createCLIDeps = () => {
29377
29716
  "Cache-Control": await cacheControlHeaderFromConfig(),
29378
29717
  ...selectedAccount.selector === void 0 ? {} : { "x-social-account-id": selectedAccount.selector }
29379
29718
  };
29380
- const data = await api.get(`x/users/by/username/${encodeURIComponent(handle)}`, { headers }).json();
29381
- if (!data || typeof data !== "object" || !("data" in data)) throw new Error(`Could not resolve X handle ${input}.`);
29719
+ const data = await api.get(`x/users/by/username/${encodeURIComponent(inputUsername)}`, { headers }).json();
29720
+ if (!data || typeof data !== "object" || !("data" in data)) throw new Error(`Could not resolve X username ${input}.`);
29382
29721
  const user = data.data;
29383
- if (!user || typeof user !== "object" || !("id" in user)) throw new Error(`Could not resolve X handle ${input}.`);
29722
+ if (!user || typeof user !== "object" || !("id" in user)) throw new Error(`Could not resolve X username ${input}.`);
29384
29723
  return {
29385
29724
  id: String(user.id),
29386
- url: `https://x.com/${"username" in user && typeof user.username === "string" ? user.username : handle}`
29725
+ url: `https://x.com/${"username" in user && typeof user.username === "string" ? user.username : inputUsername}`
29387
29726
  };
29388
29727
  };
29389
29728
  const lookupLinkedinProfile = async (input, account, selectedAccount) => {
@@ -29474,7 +29813,7 @@ const createCLIDeps = () => {
29474
29813
  return {
29475
29814
  account: {
29476
29815
  platform: "x",
29477
- handle: selected.handle,
29816
+ username: selected.username,
29478
29817
  selector: accountSelectorFor(selected)
29479
29818
  },
29480
29819
  accountIdHeader: accountSelectorFor(selected),
@@ -29491,13 +29830,13 @@ const createCLIDeps = () => {
29491
29830
  if (!selected) throw account ? new NotFoundError("linkedin_account_not_found") : /* @__PURE__ */ new Error("linkedin_account_not_connected");
29492
29831
  const profile = await lookupLinkedinProfile("me", account, {
29493
29832
  platform: "linkedin",
29494
- handle: selected.handle,
29833
+ username: selected.username,
29495
29834
  selector: accountSelectorFor(selected)
29496
29835
  });
29497
29836
  return {
29498
29837
  account: {
29499
29838
  platform: "linkedin",
29500
- handle: selected.handle,
29839
+ username: selected.username,
29501
29840
  selector: accountSelectorFor(selected)
29502
29841
  },
29503
29842
  accountIdHeader: accountSelectorFor(selected),
@@ -29704,7 +30043,6 @@ const compactSchemaForCommand = (command, path) => {
29704
30043
  const label = commandLabel(command, path);
29705
30044
  const { method, capability, mutates } = declaredMetadataFor(command, contractFor(command, label), label, { require: false });
29706
30045
  return {
29707
- schemaVersion: "1.0",
29708
30046
  name: path.join(".") || commandName(command, "social"),
29709
30047
  description: commandDescription(command),
29710
30048
  ...method ? { method } : {},
@@ -29724,7 +30062,6 @@ const compactLeafSchemaForCommand = (command, path) => {
29724
30062
  if (capability === void 0 || mutates === void 0) throw new Error(`Missing CLI contract metadata for ${label}.`);
29725
30063
  const pagination = metadataValueFor(contract.pagination ?? command.meta?.pagination, `${label}.pagination`);
29726
30064
  return {
29727
- schemaVersion: "1.0",
29728
30065
  path: path.join(" "),
29729
30066
  name: path.join(".") || commandName(command, "social"),
29730
30067
  description: commandDescription(command),
@@ -29751,14 +30088,12 @@ const schemaForCommand = (command, path) => {
29751
30088
  const pagination = metadataValueFor(contract.pagination ?? command.meta?.pagination, `${label}.pagination`);
29752
30089
  if (pagination === void 0) throw new Error(`Missing CLI contract pagination for ${label}.`);
29753
30090
  return {
29754
- schemaVersion: "1.0",
29755
30091
  name: path.join(".") || commandName(command, "social"),
29756
30092
  description: commandDescription(command),
29757
30093
  ...method ? { method } : {},
29758
30094
  ...inputSourceFor(command),
29759
30095
  args: metadataValueFor(contract.args, `${label}.args`) ?? positionalArgs,
29760
30096
  flags: metadataValueFor(contract.flags, `${label}.flags`) ?? flags,
29761
- params: metadataValueFor(contract.params, `${label}.params`) ?? args,
29762
30097
  inputSchema: inputSchemaFor(command, contract, label, args),
29763
30098
  outputSchema,
29764
30099
  auth: authFor(contract.auth ?? command.meta?.auth, capability),
@@ -29776,9 +30111,11 @@ const schemaForCommand = (command, path) => {
29776
30111
  };
29777
30112
  const treeForCommand = (command, path = []) => {
29778
30113
  const subCommands = childCommandsFor(command);
30114
+ const entries = Object.entries(subCommands).filter(([, subCommand]) => !isHiddenCommand(subCommand));
30115
+ const visibleSubCommands = Object.fromEntries(entries.map(([name, subCommand]) => [name, treeForCommand(subCommand, [...path, commandName(subCommand, name)])]));
29779
30116
  return {
29780
30117
  ...compactSchemaForCommand(command, path),
29781
- subCommands: Object.fromEntries(Object.entries(subCommands).filter(([, subCommand]) => !isHiddenCommand(subCommand)).map(([name, subCommand]) => [name, treeForCommand(subCommand, [...path, commandName(subCommand, name)])]))
30118
+ ...entries.length > 0 ? { subCommands: visibleSubCommands } : {}
29782
30119
  };
29783
30120
  };
29784
30121
  const leafSchemasForCommand = (command, path = []) => {
@@ -29864,11 +30201,20 @@ const schemaCommand = defineCommand({
29864
30201
  return;
29865
30202
  }
29866
30203
  if (parts.length === 0) {
29867
- printData(treeForCommand(rootCommand));
30204
+ printData({
30205
+ schemaVersion: "1.0",
30206
+ ...treeForCommand(rootCommand)
30207
+ });
29868
30208
  return;
29869
30209
  }
29870
30210
  const { command, path } = commandAtPath(rootCommand, parts);
29871
- printData(command.subCommands && typeof command.run !== "function" ? treeForCommand(command, path) : schemaForCommand(command, path));
30211
+ printData(command.subCommands && typeof command.run !== "function" ? {
30212
+ schemaVersion: "1.0",
30213
+ ...treeForCommand(command, path)
30214
+ } : {
30215
+ schemaVersion: "1.0",
30216
+ ...schemaForCommand(command, path)
30217
+ });
29872
30218
  }
29873
30219
  });
29874
30220
  //#endregion
@@ -29975,7 +30321,7 @@ const unknownSubCommandFor = async (command, rawArgs) => {
29975
30321
  * `commandForUsage` walk but collects names.
29976
30322
  *
29977
30323
  * PRIVACY: only tokens that resolve to a real subcommand are appended. The first
29978
- * arg that does not resolve (a positional value: tweet ID, handle, query text,
30324
+ * arg that does not resolve (a positional value: tweet ID, username, query text,
29979
30325
  * flag value) breaks the walk and is NEVER captured. Never returns rawArgs or
29980
30326
  * flag values.
29981
30327
  */