birdclaw 0.8.2 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +2 -1
  3. package/src/cli/command-context.ts +17 -0
  4. package/src/cli/register-analysis.ts +500 -0
  5. package/src/cli/register-compose.ts +40 -0
  6. package/src/cli/register-graph.ts +132 -0
  7. package/src/cli/register-inbox.ts +41 -0
  8. package/src/cli/register-storage.ts +106 -0
  9. package/src/cli.ts +30 -750
  10. package/src/components/AccountSwitcher.tsx +7 -15
  11. package/src/components/AvatarChip.tsx +1 -1
  12. package/src/components/AvatarPreload.ts +149 -0
  13. package/src/components/MarkdownCitations.tsx +680 -0
  14. package/src/components/MarkdownViewer.tsx +8 -674
  15. package/src/components/ProfileAnalysisClient.ts +191 -0
  16. package/src/components/ProfileAnalysisStream.tsx +16 -185
  17. package/src/components/ProfilePreview.tsx +2 -0
  18. package/src/components/links-controller.ts +162 -0
  19. package/src/components/links-model.ts +198 -0
  20. package/src/components/network-map-controller.ts +84 -0
  21. package/src/components/network-map-model.ts +255 -0
  22. package/src/components/useTimelineRouteData.ts +105 -235
  23. package/src/lib/analysis-runtime.ts +238 -0
  24. package/src/lib/api-client.ts +16 -215
  25. package/src/lib/api-contracts.ts +328 -0
  26. package/src/lib/archive-import-plan.ts +102 -0
  27. package/src/lib/archive-import.ts +170 -239
  28. package/src/lib/authored-live.ts +75 -120
  29. package/src/lib/backup.ts +335 -424
  30. package/src/lib/blocks-write.ts +30 -26
  31. package/src/lib/blocks.ts +18 -20
  32. package/src/lib/database-metrics.ts +88 -0
  33. package/src/lib/database-migrations.ts +34 -0
  34. package/src/lib/database-schema.ts +312 -0
  35. package/src/lib/database-writer.ts +69 -0
  36. package/src/lib/db.ts +84 -330
  37. package/src/lib/dm-read-model.ts +533 -0
  38. package/src/lib/dms-live.ts +34 -97
  39. package/src/lib/follow-graph.ts +17 -27
  40. package/src/lib/import-repository.ts +138 -0
  41. package/src/lib/inbox.ts +2 -1
  42. package/src/lib/live-sync-engine.ts +209 -0
  43. package/src/lib/live-transport-gateway.ts +128 -0
  44. package/src/lib/mention-threads-live.ts +90 -177
  45. package/src/lib/mentions-export.ts +1 -1
  46. package/src/lib/mentions-live.ts +57 -181
  47. package/src/lib/moderation-target.ts +15 -4
  48. package/src/lib/moderation-write.ts +1 -1
  49. package/src/lib/mutes-write.ts +30 -26
  50. package/src/lib/openai-response-runtime.ts +251 -0
  51. package/src/lib/paginated-sync.ts +93 -0
  52. package/src/lib/period-digest.ts +116 -304
  53. package/src/lib/profile-analysis.ts +36 -110
  54. package/src/lib/queries.ts +6 -2381
  55. package/src/lib/query-actions.ts +437 -0
  56. package/src/lib/query-client.tsx +47 -0
  57. package/src/lib/query-read-model-shared.ts +52 -0
  58. package/src/lib/query-read-models.ts +5 -0
  59. package/src/lib/query-resource.ts +41 -0
  60. package/src/lib/query-status.ts +164 -0
  61. package/src/lib/research.ts +1 -1
  62. package/src/lib/runtime-services.ts +20 -0
  63. package/src/lib/search-discussion.ts +75 -279
  64. package/src/lib/server-runtime-services.ts +30 -0
  65. package/src/lib/sqlite.ts +48 -12
  66. package/src/lib/streaming-ingestion.ts +240 -0
  67. package/src/lib/sync-cache.ts +6 -1
  68. package/src/lib/sync-plan.ts +175 -0
  69. package/src/lib/timeline-collections-live.ts +83 -257
  70. package/src/lib/timeline-live.ts +86 -236
  71. package/src/lib/timeline-read-model.ts +1191 -0
  72. package/src/lib/tweet-repository.ts +156 -0
  73. package/src/lib/tweet-search-live.ts +63 -167
  74. package/src/lib/web-sync.ts +67 -50
  75. package/src/lib/whois.ts +2 -1
  76. package/src/routes/__root.tsx +11 -8
  77. package/src/routes/api/action.tsx +1 -1
  78. package/src/routes/api/conversation.tsx +1 -1
  79. package/src/routes/api/query.tsx +32 -26
  80. package/src/routes/api/status.tsx +6 -4
  81. package/src/routes/api/sync.tsx +5 -2
  82. package/src/routes/blocks.tsx +97 -131
  83. package/src/routes/data-sources.tsx +17 -25
  84. package/src/routes/dms.tsx +167 -184
  85. package/src/routes/inbox.tsx +63 -57
  86. package/src/routes/links.tsx +31 -394
  87. package/src/routes/network-map.tsx +41 -344
  88. package/src/routes/rate-limits.tsx +17 -21
  89. package/src/lib/client-cache.ts +0 -109
package/src/lib/backup.ts CHANGED
@@ -8,7 +8,13 @@ import { Data, Effect } from "effect";
8
8
  import type { Database } from "./sqlite";
9
9
  import { getBirdclawConfig } from "./config";
10
10
  import { getNativeDb } from "./db";
11
+ import { databaseWriteEffect } from "./database-writer";
11
12
  import { runEffectPromise, tryPromise } from "./effect-runtime";
13
+ import { getImportRepository } from "./import-repository";
14
+ import {
15
+ collectIngestionSourcesEffect,
16
+ streamJsonLines,
17
+ } from "./streaming-ingestion";
12
18
  import { safeHttpUrl } from "./url-safety";
13
19
 
14
20
  const execFileAsync = promisify(execFile);
@@ -1252,36 +1258,35 @@ function assertNoSymlinkAncestorEffect(
1252
1258
  });
1253
1259
  }
1254
1260
 
1255
- function readJsonlFileEffect(
1256
- repoPath: string,
1257
- relativePath: string,
1258
- ): Effect.Effect<JsonRecord[], unknown> {
1259
- return Effect.gen(function* () {
1260
- const filePath = yield* trySync(() =>
1261
- resolveBackupFilePath(repoPath, relativePath),
1262
- );
1263
- yield* assertReadableBackupFileEffect(repoPath, filePath, relativePath);
1264
- const content = yield* tryPromise(() => fs.readFile(filePath, "utf8"));
1265
- return yield* trySync(() =>
1266
- content
1267
- .split("\n")
1268
- .filter((line) => line.length > 0)
1269
- .map((line) => JSON.parse(line) as JsonRecord),
1270
- );
1271
- });
1272
- }
1273
-
1274
1261
  function readJsonlFilesEffect(
1275
1262
  repoPath: string,
1276
1263
  relativePaths: string[],
1277
1264
  ): Effect.Effect<JsonRecord[], unknown> {
1278
1265
  return Effect.gen(function* () {
1279
- const nestedRows = yield* Effect.forEach(
1266
+ const sources = yield* Effect.forEach(
1280
1267
  relativePaths,
1281
- (relativePath) => readJsonlFileEffect(repoPath, relativePath),
1268
+ (relativePath) =>
1269
+ Effect.gen(function* () {
1270
+ const filePath = yield* trySync(() =>
1271
+ resolveBackupFilePath(repoPath, relativePath),
1272
+ );
1273
+ yield* assertReadableBackupFileEffect(
1274
+ repoPath,
1275
+ filePath,
1276
+ relativePath,
1277
+ );
1278
+ return {
1279
+ id: relativePath,
1280
+ stream: async function* () {
1281
+ for await (const row of streamJsonLines(filePath)) {
1282
+ yield row.value as JsonRecord;
1283
+ }
1284
+ },
1285
+ };
1286
+ }),
1282
1287
  { concurrency: "unbounded" },
1283
1288
  );
1284
- return nestedRows.flat();
1289
+ return yield* collectIngestionSourcesEffect(sources);
1285
1290
  });
1286
1291
  }
1287
1292
 
@@ -1336,18 +1341,6 @@ function rowsForManifestPath(
1336
1341
  .sort();
1337
1342
  }
1338
1343
 
1339
- function insertRows(
1340
- db: Database,
1341
- sql: string,
1342
- rows: JsonRecord[],
1343
- keys: string[],
1344
- ) {
1345
- const statement = db.prepare(sql);
1346
- for (const row of rows) {
1347
- statement.run(...keys.map((key) => row[key] ?? null));
1348
- }
1349
- }
1350
-
1351
1344
  const JSON_URL_KEYS = new Set([
1352
1345
  "url",
1353
1346
  "expandedUrl",
@@ -1427,68 +1420,6 @@ function sanitizeImportedUrlExpansions(rows: JsonRecord[]) {
1427
1420
  });
1428
1421
  }
1429
1422
 
1430
- function readFtsIds(
1431
- db: Database,
1432
- tableName: "tweets_fts" | "dm_fts",
1433
- idColumn: "tweet_id" | "message_id",
1434
- ) {
1435
- const rows = db
1436
- .prepare(`select ${idColumn} as id from ${tableName}`)
1437
- .all() as { id: string }[];
1438
- return new Set(rows.map((row) => row.id));
1439
- }
1440
-
1441
- function insertFtsRows(
1442
- db: Database,
1443
- tableName: "tweets_fts" | "dm_fts",
1444
- idColumn: "tweet_id" | "message_id",
1445
- rows: JsonRecord[],
1446
- idKey: string,
1447
- textKey: string,
1448
- existingIds = new Set<string>(),
1449
- ) {
1450
- const statement = db.prepare(
1451
- `insert into ${tableName} (${idColumn}, text) values (?, ?)`,
1452
- );
1453
- for (const row of rows) {
1454
- const id = row[idKey];
1455
- if (typeof id !== "string" || existingIds.has(id)) {
1456
- continue;
1457
- }
1458
- const text = row[textKey];
1459
- statement.run(id, typeof text === "string" ? text : "");
1460
- existingIds.add(id);
1461
- }
1462
- }
1463
-
1464
- function clearBackupImportData(db: Database) {
1465
- db.exec(`
1466
- delete from follow_events;
1467
- delete from follow_edges;
1468
- delete from follow_snapshot_members;
1469
- delete from follow_snapshots;
1470
- delete from ai_scores;
1471
- delete from tweet_actions;
1472
- delete from tweet_account_edges;
1473
- delete from tweet_collections;
1474
- delete from link_occurrences;
1475
- delete from url_expansions;
1476
- delete from blocks;
1477
- delete from mutes;
1478
- delete from dm_fts;
1479
- delete from tweets_fts;
1480
- delete from dm_messages;
1481
- delete from dm_conversations;
1482
- delete from tweets;
1483
- delete from profile_bio_entities;
1484
- delete from profile_snapshots;
1485
- delete from profile_affiliations;
1486
- delete from profiles;
1487
- delete from accounts;
1488
- delete from sync_cache;
1489
- `);
1490
- }
1491
-
1492
1423
  export function importBackupEffect({
1493
1424
  repoPath,
1494
1425
  db: providedDb,
@@ -1538,23 +1469,27 @@ export function importBackupEffect({
1538
1469
  const sanitizedUrlExpansions = yield* trySync(() =>
1539
1470
  sanitizeImportedUrlExpansions(urlExpansions),
1540
1471
  );
1541
-
1542
- const fingerprint = yield* trySync(() => {
1543
- db.transaction(() => {
1544
- if (mode === "replace") {
1545
- clearBackupImportData(db);
1546
- }
1547
- const tweetFtsIds =
1548
- mode === "replace"
1549
- ? new Set<string>()
1550
- : readFtsIds(db, "tweets_fts", "tweet_id");
1551
- const dmFtsIds =
1552
- mode === "replace"
1553
- ? new Set<string>()
1554
- : readFtsIds(db, "dm_fts", "message_id");
1555
- insertRows(
1556
- db,
1557
- `
1472
+ const fingerprint = yield* databaseWriteEffect((writeDb) => {
1473
+ const repository = getImportRepository(writeDb);
1474
+ if (mode === "replace") {
1475
+ repository.clearBackupImport();
1476
+ }
1477
+ const tweetFtsIds =
1478
+ mode === "replace"
1479
+ ? new Set<string>()
1480
+ : repository.readFtsIds({
1481
+ table: "tweets_fts",
1482
+ idColumn: "tweet_id",
1483
+ });
1484
+ const dmFtsIds =
1485
+ mode === "replace"
1486
+ ? new Set<string>()
1487
+ : repository.readFtsIds({
1488
+ table: "dm_fts",
1489
+ idColumn: "message_id",
1490
+ });
1491
+ repository.insertRows(
1492
+ `
1558
1493
  insert into accounts (id, name, handle, external_user_id, transport, is_default, created_at)
1559
1494
  values (?, ?, ?, ?, ?, ?, ?)
1560
1495
  on conflict(id) do update set
@@ -1565,20 +1500,19 @@ export function importBackupEffect({
1565
1500
  is_default = max(accounts.is_default, excluded.is_default),
1566
1501
  created_at = min(accounts.created_at, excluded.created_at)
1567
1502
  `,
1568
- accounts,
1569
- [
1570
- "id",
1571
- "name",
1572
- "handle",
1573
- "external_user_id",
1574
- "transport",
1575
- "is_default",
1576
- "created_at",
1577
- ],
1578
- );
1579
- insertRows(
1580
- db,
1581
- `
1503
+ accounts,
1504
+ [
1505
+ "id",
1506
+ "name",
1507
+ "handle",
1508
+ "external_user_id",
1509
+ "transport",
1510
+ "is_default",
1511
+ "created_at",
1512
+ ],
1513
+ );
1514
+ repository.insertRows(
1515
+ `
1582
1516
  insert into profile_snapshots (
1583
1517
  profile_id, snapshot_hash, observed_at, last_seen_at, source, handle,
1584
1518
  display_name, bio, location, url, verified_type, followers_count,
@@ -1592,28 +1526,27 @@ export function importBackupEffect({
1592
1526
  else profile_snapshots.raw_json
1593
1527
  end
1594
1528
  `,
1595
- profileSnapshots,
1596
- [
1597
- "profile_id",
1598
- "snapshot_hash",
1599
- "observed_at",
1600
- "last_seen_at",
1601
- "source",
1602
- "handle",
1603
- "display_name",
1604
- "bio",
1605
- "location",
1606
- "url",
1607
- "verified_type",
1608
- "followers_count",
1609
- "following_count",
1610
- "affiliations_json",
1611
- "raw_json",
1612
- ],
1613
- );
1614
- insertRows(
1615
- db,
1616
- `
1529
+ profileSnapshots,
1530
+ [
1531
+ "profile_id",
1532
+ "snapshot_hash",
1533
+ "observed_at",
1534
+ "last_seen_at",
1535
+ "source",
1536
+ "handle",
1537
+ "display_name",
1538
+ "bio",
1539
+ "location",
1540
+ "url",
1541
+ "verified_type",
1542
+ "followers_count",
1543
+ "following_count",
1544
+ "affiliations_json",
1545
+ "raw_json",
1546
+ ],
1547
+ );
1548
+ repository.insertRows(
1549
+ `
1617
1550
  insert into profile_bio_entities (
1618
1551
  profile_id, kind, value, source, is_active, first_seen_at, last_seen_at, raw_json
1619
1552
  ) values (?, ?, ?, coalesce(?, 'backup'), coalesce(?, 1), ?, ?, coalesce(?, '{}'))
@@ -1626,21 +1559,20 @@ export function importBackupEffect({
1626
1559
  else profile_bio_entities.raw_json
1627
1560
  end
1628
1561
  `,
1629
- profileBioEntities,
1630
- [
1631
- "profile_id",
1632
- "kind",
1633
- "value",
1634
- "source",
1635
- "is_active",
1636
- "first_seen_at",
1637
- "last_seen_at",
1638
- "raw_json",
1639
- ],
1640
- );
1641
- insertRows(
1642
- db,
1643
- `
1562
+ profileBioEntities,
1563
+ [
1564
+ "profile_id",
1565
+ "kind",
1566
+ "value",
1567
+ "source",
1568
+ "is_active",
1569
+ "first_seen_at",
1570
+ "last_seen_at",
1571
+ "raw_json",
1572
+ ],
1573
+ );
1574
+ repository.insertRows(
1575
+ `
1644
1576
  insert into profiles (
1645
1577
  id, handle, display_name, bio, followers_count, following_count,
1646
1578
  public_metrics_json, avatar_hue, avatar_url, location, url,
@@ -1671,28 +1603,27 @@ export function importBackupEffect({
1671
1603
  end,
1672
1604
  created_at = min(profiles.created_at, excluded.created_at)
1673
1605
  `,
1674
- profiles,
1675
- [
1676
- "id",
1677
- "handle",
1678
- "display_name",
1679
- "bio",
1680
- "followers_count",
1681
- "following_count",
1682
- "public_metrics_json",
1683
- "avatar_hue",
1684
- "avatar_url",
1685
- "location",
1686
- "url",
1687
- "verified_type",
1688
- "entities_json",
1689
- "raw_json",
1690
- "created_at",
1691
- ],
1692
- );
1693
- insertRows(
1694
- db,
1695
- `
1606
+ profiles,
1607
+ [
1608
+ "id",
1609
+ "handle",
1610
+ "display_name",
1611
+ "bio",
1612
+ "followers_count",
1613
+ "following_count",
1614
+ "public_metrics_json",
1615
+ "avatar_hue",
1616
+ "avatar_url",
1617
+ "location",
1618
+ "url",
1619
+ "verified_type",
1620
+ "entities_json",
1621
+ "raw_json",
1622
+ "created_at",
1623
+ ],
1624
+ );
1625
+ repository.insertRows(
1626
+ `
1696
1627
  insert into profile_affiliations (
1697
1628
  subject_profile_id, organization_profile_id, organization_name,
1698
1629
  organization_handle, badge_url, url, label, source, is_active,
@@ -1713,26 +1644,25 @@ export function importBackupEffect({
1713
1644
  end,
1714
1645
  updated_at = excluded.updated_at
1715
1646
  `,
1716
- profileAffiliations,
1717
- [
1718
- "subject_profile_id",
1719
- "organization_profile_id",
1720
- "organization_name",
1721
- "organization_handle",
1722
- "badge_url",
1723
- "url",
1724
- "label",
1725
- "source",
1726
- "is_active",
1727
- "first_seen_at",
1728
- "last_seen_at",
1729
- "raw_json",
1730
- "updated_at",
1731
- ],
1732
- );
1733
- insertRows(
1734
- db,
1735
- `
1647
+ profileAffiliations,
1648
+ [
1649
+ "subject_profile_id",
1650
+ "organization_profile_id",
1651
+ "organization_name",
1652
+ "organization_handle",
1653
+ "badge_url",
1654
+ "url",
1655
+ "label",
1656
+ "source",
1657
+ "is_active",
1658
+ "first_seen_at",
1659
+ "last_seen_at",
1660
+ "raw_json",
1661
+ "updated_at",
1662
+ ],
1663
+ );
1664
+ repository.insertRows(
1665
+ `
1736
1666
  insert into follow_snapshots (
1737
1667
  id, account_id, direction, source, status, page_count, result_count,
1738
1668
  started_at, completed_at, raw_meta_json
@@ -1751,23 +1681,22 @@ export function importBackupEffect({
1751
1681
  else follow_snapshots.raw_meta_json
1752
1682
  end
1753
1683
  `,
1754
- followSnapshots,
1755
- [
1756
- "id",
1757
- "account_id",
1758
- "direction",
1759
- "source",
1760
- "status",
1761
- "page_count",
1762
- "result_count",
1763
- "started_at",
1764
- "completed_at",
1765
- "raw_meta_json",
1766
- ],
1767
- );
1768
- insertRows(
1769
- db,
1770
- `
1684
+ followSnapshots,
1685
+ [
1686
+ "id",
1687
+ "account_id",
1688
+ "direction",
1689
+ "source",
1690
+ "status",
1691
+ "page_count",
1692
+ "result_count",
1693
+ "started_at",
1694
+ "completed_at",
1695
+ "raw_meta_json",
1696
+ ],
1697
+ );
1698
+ repository.insertRows(
1699
+ `
1771
1700
  insert into follow_snapshot_members (
1772
1701
  snapshot_id, profile_id, external_user_id, position
1773
1702
  ) values (?, ?, ?, coalesce(?, 0))
@@ -1775,12 +1704,11 @@ export function importBackupEffect({
1775
1704
  external_user_id = coalesce(nullif(excluded.external_user_id, ''), follow_snapshot_members.external_user_id),
1776
1705
  position = excluded.position
1777
1706
  `,
1778
- followSnapshotMembers,
1779
- ["snapshot_id", "profile_id", "external_user_id", "position"],
1780
- );
1781
- insertRows(
1782
- db,
1783
- `
1707
+ followSnapshotMembers,
1708
+ ["snapshot_id", "profile_id", "external_user_id", "position"],
1709
+ );
1710
+ repository.insertRows(
1711
+ `
1784
1712
  insert into follow_edges (
1785
1713
  account_id, direction, profile_id, external_user_id, source, current,
1786
1714
  first_seen_at, last_seen_at, ended_at, updated_at
@@ -1800,23 +1728,22 @@ export function importBackupEffect({
1800
1728
  end,
1801
1729
  updated_at = max(follow_edges.updated_at, excluded.updated_at)
1802
1730
  `,
1803
- followEdges,
1804
- [
1805
- "account_id",
1806
- "direction",
1807
- "profile_id",
1808
- "external_user_id",
1809
- "source",
1810
- "current",
1811
- "first_seen_at",
1812
- "last_seen_at",
1813
- "ended_at",
1814
- "updated_at",
1815
- ],
1816
- );
1817
- insertRows(
1818
- db,
1819
- `
1731
+ followEdges,
1732
+ [
1733
+ "account_id",
1734
+ "direction",
1735
+ "profile_id",
1736
+ "external_user_id",
1737
+ "source",
1738
+ "current",
1739
+ "first_seen_at",
1740
+ "last_seen_at",
1741
+ "ended_at",
1742
+ "updated_at",
1743
+ ],
1744
+ );
1745
+ repository.insertRows(
1746
+ `
1820
1747
  insert into follow_events (
1821
1748
  id, account_id, direction, profile_id, external_user_id, kind, event_at,
1822
1749
  snapshot_id
@@ -1830,21 +1757,20 @@ export function importBackupEffect({
1830
1757
  event_at = coalesce(nullif(excluded.event_at, ''), follow_events.event_at),
1831
1758
  snapshot_id = coalesce(nullif(excluded.snapshot_id, ''), follow_events.snapshot_id)
1832
1759
  `,
1833
- followEvents,
1834
- [
1835
- "id",
1836
- "account_id",
1837
- "direction",
1838
- "profile_id",
1839
- "external_user_id",
1840
- "kind",
1841
- "event_at",
1842
- "snapshot_id",
1843
- ],
1844
- );
1845
- insertRows(
1846
- db,
1847
- `
1760
+ followEvents,
1761
+ [
1762
+ "id",
1763
+ "account_id",
1764
+ "direction",
1765
+ "profile_id",
1766
+ "external_user_id",
1767
+ "kind",
1768
+ "event_at",
1769
+ "snapshot_id",
1770
+ ],
1771
+ );
1772
+ repository.insertRows(
1773
+ `
1848
1774
  insert into tweets (
1849
1775
  id, account_id, author_profile_id, kind, text, created_at, is_replied,
1850
1776
  reply_to_id, like_count, media_count, bookmarked, liked, entities_json,
@@ -1876,37 +1802,34 @@ export function importBackupEffect({
1876
1802
  end,
1877
1803
  quoted_tweet_id = coalesce(excluded.quoted_tweet_id, tweets.quoted_tweet_id)
1878
1804
  `,
1879
- sanitizedTweets,
1880
- [
1881
- "id",
1882
- "account_id",
1883
- "author_profile_id",
1884
- "kind",
1885
- "text",
1886
- "created_at",
1887
- "is_replied",
1888
- "reply_to_id",
1889
- "like_count",
1890
- "media_count",
1891
- "bookmarked",
1892
- "liked",
1893
- "entities_json",
1894
- "media_json",
1895
- "quoted_tweet_id",
1896
- ],
1897
- );
1898
- insertFtsRows(
1899
- db,
1900
- "tweets_fts",
1901
- "tweet_id",
1902
- sanitizedTweets,
1805
+ sanitizedTweets,
1806
+ [
1903
1807
  "id",
1808
+ "account_id",
1809
+ "author_profile_id",
1810
+ "kind",
1904
1811
  "text",
1905
- tweetFtsIds,
1906
- );
1907
- insertRows(
1908
- db,
1909
- `
1812
+ "created_at",
1813
+ "is_replied",
1814
+ "reply_to_id",
1815
+ "like_count",
1816
+ "media_count",
1817
+ "bookmarked",
1818
+ "liked",
1819
+ "entities_json",
1820
+ "media_json",
1821
+ "quoted_tweet_id",
1822
+ ],
1823
+ );
1824
+ repository.insertFtsRows({
1825
+ target: { table: "tweets_fts", idColumn: "tweet_id" },
1826
+ rows: sanitizedTweets,
1827
+ idKey: "id",
1828
+ textKey: "text",
1829
+ existingIds: tweetFtsIds,
1830
+ });
1831
+ repository.insertRows(
1832
+ `
1910
1833
  insert into tweet_collections (
1911
1834
  account_id, tweet_id, kind, collected_at, source, raw_json, updated_at
1912
1835
  ) values (?, ?, ?, ?, ?, ?, ?)
@@ -1919,20 +1842,19 @@ export function importBackupEffect({
1919
1842
  end,
1920
1843
  updated_at = max(tweet_collections.updated_at, excluded.updated_at)
1921
1844
  `,
1922
- collections,
1923
- [
1924
- "account_id",
1925
- "tweet_id",
1926
- "kind",
1927
- "collected_at",
1928
- "source",
1929
- "raw_json",
1930
- "updated_at",
1931
- ],
1932
- );
1933
- insertRows(
1934
- db,
1935
- `
1845
+ collections,
1846
+ [
1847
+ "account_id",
1848
+ "tweet_id",
1849
+ "kind",
1850
+ "collected_at",
1851
+ "source",
1852
+ "raw_json",
1853
+ "updated_at",
1854
+ ],
1855
+ );
1856
+ repository.insertRows(
1857
+ `
1936
1858
  insert into tweet_account_edges (
1937
1859
  account_id, tweet_id, kind, first_seen_at, last_seen_at, seen_count,
1938
1860
  source, raw_json, updated_at
@@ -1948,22 +1870,21 @@ export function importBackupEffect({
1948
1870
  end,
1949
1871
  updated_at = max(tweet_account_edges.updated_at, excluded.updated_at)
1950
1872
  `,
1951
- timelineEdges,
1952
- [
1953
- "account_id",
1954
- "tweet_id",
1955
- "kind",
1956
- "first_seen_at",
1957
- "last_seen_at",
1958
- "seen_count",
1959
- "source",
1960
- "raw_json",
1961
- "updated_at",
1962
- ],
1963
- );
1964
- insertRows(
1965
- db,
1966
- `
1873
+ timelineEdges,
1874
+ [
1875
+ "account_id",
1876
+ "tweet_id",
1877
+ "kind",
1878
+ "first_seen_at",
1879
+ "last_seen_at",
1880
+ "seen_count",
1881
+ "source",
1882
+ "raw_json",
1883
+ "updated_at",
1884
+ ],
1885
+ );
1886
+ repository.insertRows(
1887
+ `
1967
1888
  insert into dm_conversations (
1968
1889
  id, account_id, participant_profile_id, title, inbox_kind, last_message_at, unread_count, needs_reply
1969
1890
  ) values (?, ?, ?, ?, coalesce(?, 'accepted'), ?, ?, ?)
@@ -1980,21 +1901,20 @@ export function importBackupEffect({
1980
1901
  unread_count = max(dm_conversations.unread_count, excluded.unread_count),
1981
1902
  needs_reply = max(dm_conversations.needs_reply, excluded.needs_reply)
1982
1903
  `,
1983
- conversations,
1984
- [
1985
- "id",
1986
- "account_id",
1987
- "participant_profile_id",
1988
- "title",
1989
- "inbox_kind",
1990
- "last_message_at",
1991
- "unread_count",
1992
- "needs_reply",
1993
- ],
1994
- );
1995
- insertRows(
1996
- db,
1997
- `
1904
+ conversations,
1905
+ [
1906
+ "id",
1907
+ "account_id",
1908
+ "participant_profile_id",
1909
+ "title",
1910
+ "inbox_kind",
1911
+ "last_message_at",
1912
+ "unread_count",
1913
+ "needs_reply",
1914
+ ],
1915
+ );
1916
+ repository.insertRows(
1917
+ `
1998
1918
  insert into dm_messages (
1999
1919
  id, conversation_id, sender_profile_id, text, created_at, direction, is_replied, media_count
2000
1920
  ) values (?, ?, ?, ?, ?, ?, ?, ?)
@@ -2007,30 +1927,27 @@ export function importBackupEffect({
2007
1927
  is_replied = max(dm_messages.is_replied, excluded.is_replied),
2008
1928
  media_count = max(dm_messages.media_count, excluded.media_count)
2009
1929
  `,
2010
- messages,
2011
- [
2012
- "id",
2013
- "conversation_id",
2014
- "sender_profile_id",
2015
- "text",
2016
- "created_at",
2017
- "direction",
2018
- "is_replied",
2019
- "media_count",
2020
- ],
2021
- );
2022
- insertFtsRows(
2023
- db,
2024
- "dm_fts",
2025
- "message_id",
2026
- messages,
1930
+ messages,
1931
+ [
2027
1932
  "id",
1933
+ "conversation_id",
1934
+ "sender_profile_id",
2028
1935
  "text",
2029
- dmFtsIds,
2030
- );
2031
- insertRows(
2032
- db,
2033
- `
1936
+ "created_at",
1937
+ "direction",
1938
+ "is_replied",
1939
+ "media_count",
1940
+ ],
1941
+ );
1942
+ repository.insertFtsRows({
1943
+ target: { table: "dm_fts", idColumn: "message_id" },
1944
+ rows: messages,
1945
+ idKey: "id",
1946
+ textKey: "text",
1947
+ existingIds: dmFtsIds,
1948
+ });
1949
+ repository.insertRows(
1950
+ `
2034
1951
  insert into url_expansions (
2035
1952
  short_url, expanded_url, final_url, status, expanded_tweet_id,
2036
1953
  expanded_handle, title, description, image_url, site_name, error, source,
@@ -2050,26 +1967,25 @@ export function importBackupEffect({
2050
1967
  source = excluded.source,
2051
1968
  updated_at = excluded.updated_at
2052
1969
  `,
2053
- sanitizedUrlExpansions,
2054
- [
2055
- "short_url",
2056
- "expanded_url",
2057
- "final_url",
2058
- "status",
2059
- "expanded_tweet_id",
2060
- "expanded_handle",
2061
- "title",
2062
- "description",
2063
- "image_url",
2064
- "site_name",
2065
- "error",
2066
- "source",
2067
- "updated_at",
2068
- ],
2069
- );
2070
- insertRows(
2071
- db,
2072
- `
1970
+ sanitizedUrlExpansions,
1971
+ [
1972
+ "short_url",
1973
+ "expanded_url",
1974
+ "final_url",
1975
+ "status",
1976
+ "expanded_tweet_id",
1977
+ "expanded_handle",
1978
+ "title",
1979
+ "description",
1980
+ "image_url",
1981
+ "site_name",
1982
+ "error",
1983
+ "source",
1984
+ "updated_at",
1985
+ ],
1986
+ );
1987
+ repository.insertRows(
1988
+ `
2073
1989
  insert into link_occurrences (
2074
1990
  source_kind, source_id, source_position, short_url, account_id,
2075
1991
  conversation_id, direction, created_at
@@ -2080,45 +1996,42 @@ export function importBackupEffect({
2080
1996
  direction = excluded.direction,
2081
1997
  created_at = excluded.created_at
2082
1998
  `,
2083
- linkOccurrences,
2084
- [
2085
- "source_kind",
2086
- "source_id",
2087
- "source_position",
2088
- "short_url",
2089
- "account_id",
2090
- "conversation_id",
2091
- "direction",
2092
- "created_at",
2093
- ],
2094
- );
2095
- insertRows(
2096
- db,
2097
- `
1999
+ linkOccurrences,
2000
+ [
2001
+ "source_kind",
2002
+ "source_id",
2003
+ "source_position",
2004
+ "short_url",
2005
+ "account_id",
2006
+ "conversation_id",
2007
+ "direction",
2008
+ "created_at",
2009
+ ],
2010
+ );
2011
+ repository.insertRows(
2012
+ `
2098
2013
  insert into blocks (account_id, profile_id, source, created_at)
2099
2014
  values (?, ?, ?, ?)
2100
2015
  on conflict(account_id, profile_id) do update set
2101
2016
  source = coalesce(nullif(excluded.source, ''), blocks.source),
2102
2017
  created_at = min(blocks.created_at, excluded.created_at)
2103
2018
  `,
2104
- blocks,
2105
- ["account_id", "profile_id", "source", "created_at"],
2106
- );
2107
- insertRows(
2108
- db,
2109
- `
2019
+ blocks,
2020
+ ["account_id", "profile_id", "source", "created_at"],
2021
+ );
2022
+ repository.insertRows(
2023
+ `
2110
2024
  insert into mutes (account_id, profile_id, source, created_at)
2111
2025
  values (?, ?, ?, ?)
2112
2026
  on conflict(account_id, profile_id) do update set
2113
2027
  source = coalesce(nullif(excluded.source, ''), mutes.source),
2114
2028
  created_at = min(mutes.created_at, excluded.created_at)
2115
2029
  `,
2116
- mutes,
2117
- ["account_id", "profile_id", "source", "created_at"],
2118
- );
2119
- insertRows(
2120
- db,
2121
- `
2030
+ mutes,
2031
+ ["account_id", "profile_id", "source", "created_at"],
2032
+ );
2033
+ repository.insertRows(
2034
+ `
2122
2035
  insert into tweet_actions (id, account_id, tweet_id, kind, body, created_at)
2123
2036
  values (?, ?, ?, ?, ?, ?)
2124
2037
  on conflict(id) do update set
@@ -2128,12 +2041,11 @@ export function importBackupEffect({
2128
2041
  body = coalesce(nullif(excluded.body, ''), tweet_actions.body),
2129
2042
  created_at = min(tweet_actions.created_at, excluded.created_at)
2130
2043
  `,
2131
- actions,
2132
- ["id", "account_id", "tweet_id", "kind", "body", "created_at"],
2133
- );
2134
- insertRows(
2135
- db,
2136
- `
2044
+ actions,
2045
+ ["id", "account_id", "tweet_id", "kind", "body", "created_at"],
2046
+ );
2047
+ repository.insertRows(
2048
+ `
2137
2049
  insert into ai_scores (
2138
2050
  entity_kind, entity_id, model, score, summary, reasoning, updated_at
2139
2051
  ) values (?, ?, ?, ?, ?, ?, ?)
@@ -2144,20 +2056,19 @@ export function importBackupEffect({
2144
2056
  reasoning = coalesce(nullif(excluded.reasoning, ''), ai_scores.reasoning),
2145
2057
  updated_at = max(ai_scores.updated_at, excluded.updated_at)
2146
2058
  `,
2147
- scores,
2148
- [
2149
- "entity_kind",
2150
- "entity_id",
2151
- "model",
2152
- "score",
2153
- "summary",
2154
- "reasoning",
2155
- "updated_at",
2156
- ],
2157
- );
2158
- })();
2159
- return getBackupDatabaseFingerprint(db);
2160
- });
2059
+ scores,
2060
+ [
2061
+ "entity_kind",
2062
+ "entity_id",
2063
+ "model",
2064
+ "score",
2065
+ "summary",
2066
+ "reasoning",
2067
+ "updated_at",
2068
+ ],
2069
+ );
2070
+ return getBackupDatabaseFingerprint(writeDb);
2071
+ }, db);
2161
2072
 
2162
2073
  return {
2163
2074
  ok: true,