birdclaw 0.5.1 → 0.6.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.
Files changed (89) hide show
  1. package/CHANGELOG.md +44 -1
  2. package/README.md +50 -5
  3. package/package.json +3 -2
  4. package/scripts/browser-perf.mjs +1 -0
  5. package/scripts/start-test-server.mjs +16 -3
  6. package/src/cli.ts +376 -13
  7. package/src/components/AccountSwitcher.tsx +186 -0
  8. package/src/components/AppNav.tsx +27 -7
  9. package/src/components/AvatarChip.tsx +9 -3
  10. package/src/components/DmWorkspace.tsx +18 -8
  11. package/src/components/LinkPreviewCard.tsx +40 -18
  12. package/src/components/MarkdownViewer.tsx +452 -0
  13. package/src/components/SyncNowButton.tsx +57 -25
  14. package/src/components/ThemeSlider.tsx +55 -50
  15. package/src/components/TimelineCard.tsx +225 -93
  16. package/src/components/TimelineRouteFrame.tsx +22 -8
  17. package/src/components/TweetMediaGrid.tsx +87 -38
  18. package/src/components/TweetRichText.tsx +15 -11
  19. package/src/components/account-selection.ts +64 -0
  20. package/src/components/useTimelineRouteData.ts +23 -7
  21. package/src/lib/account-sync-job.ts +654 -0
  22. package/src/lib/actions-transport.ts +216 -146
  23. package/src/lib/api-client.ts +128 -53
  24. package/src/lib/archive-finder.ts +78 -63
  25. package/src/lib/archive-import.ts +1364 -1300
  26. package/src/lib/authored-live.ts +261 -204
  27. package/src/lib/avatar-cache.ts +159 -44
  28. package/src/lib/backup.ts +1532 -951
  29. package/src/lib/bird-actions.ts +139 -57
  30. package/src/lib/bird-command.ts +101 -28
  31. package/src/lib/bird.ts +549 -194
  32. package/src/lib/blocklist.ts +40 -23
  33. package/src/lib/blocks-write.ts +129 -80
  34. package/src/lib/blocks.ts +165 -97
  35. package/src/lib/bookmark-sync-job.ts +250 -160
  36. package/src/lib/conversation-surface.ts +79 -48
  37. package/src/lib/db.ts +33 -3
  38. package/src/lib/dms-live.ts +720 -66
  39. package/src/lib/effect-runtime.ts +45 -0
  40. package/src/lib/follow-graph.ts +224 -180
  41. package/src/lib/http-effect.ts +222 -0
  42. package/src/lib/inbox.ts +74 -43
  43. package/src/lib/link-index.ts +88 -76
  44. package/src/lib/link-insights.ts +24 -0
  45. package/src/lib/link-preview-metadata.ts +472 -52
  46. package/src/lib/media-fetch.ts +286 -213
  47. package/src/lib/mention-threads-live.ts +352 -288
  48. package/src/lib/mentions-live.ts +390 -342
  49. package/src/lib/moderation-target.ts +102 -65
  50. package/src/lib/moderation-write.ts +77 -18
  51. package/src/lib/mutes-write.ts +129 -80
  52. package/src/lib/mutes.ts +8 -1
  53. package/src/lib/openai.ts +84 -53
  54. package/src/lib/period-digest.ts +953 -0
  55. package/src/lib/profile-affiliation-hydration.ts +93 -54
  56. package/src/lib/profile-hydration.ts +124 -72
  57. package/src/lib/profile-replies.ts +60 -43
  58. package/src/lib/profile-resolver.ts +402 -294
  59. package/src/lib/queries.ts +969 -199
  60. package/src/lib/research.ts +165 -120
  61. package/src/lib/sqlite.ts +1 -0
  62. package/src/lib/timeline-collections-live.ts +204 -167
  63. package/src/lib/timeline-live.ts +60 -39
  64. package/src/lib/tweet-lookup.ts +30 -19
  65. package/src/lib/types.ts +38 -1
  66. package/src/lib/ui.ts +30 -7
  67. package/src/lib/url-expansion.ts +226 -55
  68. package/src/lib/url-safety.ts +220 -0
  69. package/src/lib/web-sync.ts +216 -148
  70. package/src/lib/whois.ts +166 -147
  71. package/src/lib/x-web.ts +102 -71
  72. package/src/lib/xurl.ts +681 -411
  73. package/src/routeTree.gen.ts +42 -0
  74. package/src/routes/__root.tsx +25 -5
  75. package/src/routes/api/action.tsx +127 -78
  76. package/src/routes/api/avatar.tsx +39 -30
  77. package/src/routes/api/blocks.tsx +26 -23
  78. package/src/routes/api/conversation.tsx +25 -14
  79. package/src/routes/api/inbox.tsx +27 -21
  80. package/src/routes/api/link-insights.tsx +31 -25
  81. package/src/routes/api/link-preview.tsx +25 -21
  82. package/src/routes/api/period-digest.tsx +123 -0
  83. package/src/routes/api/profile-hydrate.tsx +31 -28
  84. package/src/routes/api/query.tsx +79 -55
  85. package/src/routes/api/status.tsx +18 -10
  86. package/src/routes/api/sync.tsx +75 -29
  87. package/src/routes/dms.tsx +95 -28
  88. package/src/routes/inbox.tsx +32 -19
  89. package/src/routes/today.tsx +441 -0
package/src/cli.ts CHANGED
@@ -17,6 +17,11 @@ import {
17
17
  syncAuthoredTweets,
18
18
  type AuthoredSyncMode,
19
19
  } from "#/lib/authored-live";
20
+ import {
21
+ installAccountSyncLaunchAgent,
22
+ parseAccountSyncSteps,
23
+ runAccountSyncJob,
24
+ } from "#/lib/account-sync-job";
20
25
  import {
21
26
  exportBackup,
22
27
  importBackup,
@@ -29,6 +34,7 @@ import {
29
34
  installBookmarkSyncLaunchAgent,
30
35
  runBookmarkSyncJob,
31
36
  } from "#/lib/bookmark-sync-job";
37
+ import { runDirectMessageRequestMutationViaBird } from "#/lib/bird";
32
38
  import { importBlocklist } from "#/lib/blocklist";
33
39
  import {
34
40
  type ActionsTransport,
@@ -36,7 +42,11 @@ import {
36
42
  getBirdclawPaths,
37
43
  resolveMentionsDataSource,
38
44
  } from "#/lib/config";
39
- import { syncDirectMessagesViaCachedBird } from "#/lib/dms-live";
45
+ import { closeDatabase } from "#/lib/db";
46
+ import {
47
+ type DirectMessagesSyncMode,
48
+ syncDirectMessagesViaCachedBird,
49
+ } from "#/lib/dms-live";
40
50
  import { listInboxItems, scoreInbox } from "#/lib/inbox";
41
51
  import { backfillLinkIndex, searchLinks } from "#/lib/link-index";
42
52
  import { fetchTweetMedia, formatMediaFetchResult } from "#/lib/media-fetch";
@@ -47,6 +57,11 @@ import {
47
57
  exportMentionsViaCachedXurl,
48
58
  syncMentions,
49
59
  } from "#/lib/mentions-live";
60
+ import {
61
+ streamPeriodDigest,
62
+ type PeriodDigestOptions,
63
+ type PeriodDigestPreset,
64
+ } from "#/lib/period-digest";
50
65
  import {
51
66
  getFollowGraphSummary,
52
67
  listFollowEvents,
@@ -61,6 +76,7 @@ import { resolveProfilesForIds } from "#/lib/profile-resolver";
61
76
  import { inspectProfileReplies } from "#/lib/profile-replies";
62
77
  import { runResearchMode } from "#/lib/research";
63
78
  import {
79
+ applyDmRequestMutationToLocalStore,
64
80
  createDmReply,
65
81
  createPost,
66
82
  createTweetReply,
@@ -154,6 +170,37 @@ function parsePositiveIntegerOption(value: string | undefined, option: string) {
154
170
  return parsed;
155
171
  }
156
172
 
173
+ function parseDmInboxOption(
174
+ value: string | undefined,
175
+ ): "all" | "accepted" | "requests" | undefined {
176
+ const normalized = (value ?? "all").trim().toLowerCase();
177
+ if (
178
+ normalized === "all" ||
179
+ normalized === "accepted" ||
180
+ normalized === "requests"
181
+ ) {
182
+ return normalized;
183
+ }
184
+ if (normalized === "request") {
185
+ return "requests";
186
+ }
187
+ printError("--inbox must be all, accepted, or requests");
188
+ process.exitCode = 1;
189
+ return undefined;
190
+ }
191
+
192
+ function parseDmSyncModeOption(
193
+ value: string | undefined,
194
+ ): DirectMessagesSyncMode | undefined {
195
+ const normalized = (value ?? "bird").trim().toLowerCase();
196
+ if (normalized === "auto" || normalized === "bird" || normalized === "xurl") {
197
+ return normalized;
198
+ }
199
+ printError("--mode must be auto, bird, or xurl");
200
+ process.exitCode = 1;
201
+ return undefined;
202
+ }
203
+
157
204
  function parseArchiveImportSelect(value: string | undefined) {
158
205
  if (value === undefined) {
159
206
  return undefined;
@@ -209,6 +256,73 @@ function resolveActionOptions(options: { transport?: string }) {
209
256
  };
210
257
  }
211
258
 
259
+ function parseDigestPeriod(value: string | undefined): PeriodDigestPreset {
260
+ const normalized = value?.trim().toLowerCase();
261
+ if (normalized === "yesterday") return "yesterday";
262
+ if (normalized === "24h" || normalized === "day") return "24h";
263
+ if (normalized === "week" || normalized === "7d") return "week";
264
+ return "today";
265
+ }
266
+
267
+ function buildDigestOptions(
268
+ period: string | undefined,
269
+ options: {
270
+ account?: string;
271
+ includeDms?: boolean;
272
+ model?: string;
273
+ refresh?: boolean;
274
+ since?: string;
275
+ until?: string;
276
+ maxTweets?: string;
277
+ maxLinks?: string;
278
+ },
279
+ ): PeriodDigestOptions | null {
280
+ const maxTweets = parseNonNegativeIntegerOption(
281
+ options.maxTweets,
282
+ "--max-tweets",
283
+ );
284
+ if (options.maxTweets !== undefined && maxTweets === undefined) {
285
+ return null;
286
+ }
287
+ const maxLinks = parseNonNegativeIntegerOption(
288
+ options.maxLinks,
289
+ "--max-links",
290
+ );
291
+ if (options.maxLinks !== undefined && maxLinks === undefined) {
292
+ return null;
293
+ }
294
+ return {
295
+ period: parseDigestPeriod(period),
296
+ since: options.since,
297
+ until: options.until,
298
+ account: options.account,
299
+ includeDms: Boolean(options.includeDms),
300
+ refresh: Boolean(options.refresh),
301
+ model: options.model,
302
+ maxTweets,
303
+ maxLinks,
304
+ };
305
+ }
306
+
307
+ function runDigestCli(options: PeriodDigestOptions) {
308
+ const asJson = Boolean(program.opts().json);
309
+ return streamPeriodDigest(options, {
310
+ onDelta: asJson
311
+ ? undefined
312
+ : (delta) => {
313
+ process.stdout.write(delta);
314
+ },
315
+ }).then((result) => {
316
+ if (asJson) {
317
+ print(result, true);
318
+ return;
319
+ }
320
+ if (!result.markdown.endsWith("\n")) {
321
+ process.stdout.write("\n");
322
+ }
323
+ });
324
+ }
325
+
212
326
  async function enrichDmItems(
213
327
  query: Parameters<typeof listDmConversations>[0],
214
328
  options: {
@@ -260,14 +374,28 @@ async function enrichDmItems(
260
374
  }
261
375
 
262
376
  async function autoUpdateBeforeRead() {
263
- const result = await maybeAutoUpdateBackup();
377
+ let result: Awaited<ReturnType<typeof maybeAutoUpdateBackup>>;
378
+ try {
379
+ result = await maybeAutoUpdateBackup();
380
+ } catch (error) {
381
+ const message = error instanceof Error ? error.message : String(error);
382
+ console.error(`birdclaw backup auto-sync failed: ${message}`);
383
+ return;
384
+ }
264
385
  if (!result.ok) {
265
386
  console.error(`birdclaw backup auto-sync failed: ${result.error}`);
266
387
  }
267
388
  }
268
389
 
269
390
  async function autoSyncAfterWrite() {
270
- const result = await maybeAutoSyncBackup();
391
+ let result: Awaited<ReturnType<typeof maybeAutoSyncBackup>>;
392
+ try {
393
+ result = await maybeAutoSyncBackup();
394
+ } catch (error) {
395
+ const message = error instanceof Error ? error.message : String(error);
396
+ console.error(`birdclaw backup sync failed: ${message}`);
397
+ return;
398
+ }
271
399
  if (!result.ok) {
272
400
  console.error(`birdclaw backup sync failed: ${result.error}`);
273
401
  }
@@ -416,12 +544,13 @@ searchCommand
416
544
 
417
545
  searchCommand
418
546
  .command("dms <query>")
547
+ .option("--inbox <kind>", "all, accepted, or requests", "all")
419
548
  .option("--participant <value>")
420
549
  .option("--min-followers <n>", "Minimum sender follower count")
421
550
  .option("--max-followers <n>", "Maximum sender follower count")
422
551
  .option("--min-influence-score <n>", "Minimum derived influence score")
423
552
  .option("--max-influence-score <n>", "Maximum derived influence score")
424
- .option("--sort <mode>", "recent or influence", "recent")
553
+ .option("--sort <mode>", "recent or followers", "recent")
425
554
  .option(
426
555
  "--context <n>",
427
556
  "Include N messages before and after each match",
@@ -447,6 +576,10 @@ searchCommand
447
576
  if (context === undefined) {
448
577
  return;
449
578
  }
579
+ const inbox = parseDmInboxOption(options.inbox);
580
+ if (inbox === undefined) {
581
+ return;
582
+ }
450
583
  const replyFilter = options.replied
451
584
  ? "replied"
452
585
  : options.unreplied
@@ -454,6 +587,7 @@ searchCommand
454
587
  : "all";
455
588
  const dmQuery = {
456
589
  search: query,
590
+ ...(inbox !== "all" ? { inbox } : {}),
457
591
  participant: options.participant,
458
592
  minFollowers: options.minFollowers
459
593
  ? Number(options.minFollowers)
@@ -467,7 +601,10 @@ searchCommand
467
601
  maxInfluenceScore: options.maxInfluenceScore
468
602
  ? Number(options.maxInfluenceScore)
469
603
  : undefined,
470
- sort: options.sort === "influence" ? "influence" : "recent",
604
+ sort:
605
+ options.sort === "followers" || options.sort === "influence"
606
+ ? "followers"
607
+ : "recent",
471
608
  replyFilter,
472
609
  context,
473
610
  limit: Number(options.limit),
@@ -720,6 +857,40 @@ program
720
857
  );
721
858
  });
722
859
 
860
+ program
861
+ .command("today")
862
+ .description("Stream an AI digest of what happened today")
863
+ .option("--account <accountId>", "Account id")
864
+ .option("--include-dms", "Include private DM context")
865
+ .option("--model <model>", "OpenAI model id")
866
+ .option("--refresh", "Bypass the local digest cache")
867
+ .option("--max-tweets <n>", "Maximum tweet context", "300")
868
+ .option("--max-links <n>", "Maximum linked articles", "12")
869
+ .action(async (options) => {
870
+ await autoUpdateBeforeRead();
871
+ const digestOptions = buildDigestOptions("today", options);
872
+ if (!digestOptions) return;
873
+ await runDigestCli(digestOptions);
874
+ });
875
+
876
+ program
877
+ .command("digest [period]")
878
+ .description("Stream an AI digest for today, 24h, yesterday, or week")
879
+ .option("--account <accountId>", "Account id")
880
+ .option("--include-dms", "Include private DM context")
881
+ .option("--since <isoDate>", "Start of explicit window")
882
+ .option("--until <isoDate>", "End of explicit window")
883
+ .option("--model <model>", "OpenAI model id")
884
+ .option("--refresh", "Bypass the local digest cache")
885
+ .option("--max-tweets <n>", "Maximum tweet context", "300")
886
+ .option("--max-links <n>", "Maximum linked articles", "12")
887
+ .action(async (period, options) => {
888
+ await autoUpdateBeforeRead();
889
+ const digestOptions = buildDigestOptions(period, options);
890
+ if (!digestOptions) return;
891
+ await runDigestCli(digestOptions);
892
+ });
893
+
723
894
  const mentionsCommand = program
724
895
  .command("mentions")
725
896
  .description("Export local mention tweets for scripts and agents");
@@ -1033,6 +1204,92 @@ const jobsCommand = program
1033
1204
  .command("jobs")
1034
1205
  .description("Run and install background Birdclaw jobs");
1035
1206
 
1207
+ jobsCommand
1208
+ .command("sync-account")
1209
+ .description("Refresh live account timelines and append a JSONL audit entry")
1210
+ .option("--account <accountId>", "Account id")
1211
+ .option(
1212
+ "--steps <steps>",
1213
+ "Comma list: timeline,mentions,mention-threads,likes,bookmarks,dms",
1214
+ )
1215
+ .option("--mode <mode>", "auto, xurl, or bird for likes/bookmarks", "auto")
1216
+ .option("--limit <n>", "Per-page/result limit", "100")
1217
+ .option("--max-pages <n>", "Stop after N pages", "3")
1218
+ .option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
1219
+ .option("--refresh", "Bypass live-cache freshness window")
1220
+ .option(
1221
+ "--allow-bird-account",
1222
+ "Assert bird cookies match --account for Bird-backed steps",
1223
+ )
1224
+ .option("--log <path>", "Audit JSONL path")
1225
+ .action(async (options) => {
1226
+ const result = await runAccountSyncJob({
1227
+ account: options.account,
1228
+ steps: parseAccountSyncSteps(options.steps),
1229
+ mode: options.mode as TimelineCollectionMode,
1230
+ limit: Number(options.limit),
1231
+ maxPages: Number(options.maxPages),
1232
+ refresh: Boolean(options.refresh),
1233
+ cacheTtlMs: Number(options.cacheTtl) * 1000,
1234
+ allowBirdAccount: Boolean(options.allowBirdAccount),
1235
+ logPath: options.log,
1236
+ });
1237
+ print(result, true);
1238
+ if (!result.ok) {
1239
+ process.exitCode = 1;
1240
+ }
1241
+ });
1242
+
1243
+ jobsCommand
1244
+ .command("install-account-launchd")
1245
+ .description("Install a LaunchAgent that runs account sync")
1246
+ .option("--label <label>", "LaunchAgent label")
1247
+ .option("--interval-seconds <seconds>", "Launch interval", "1800")
1248
+ .option("--program <path>", "birdclaw executable or command", "birdclaw")
1249
+ .option("--account <accountId>", "Account id")
1250
+ .option(
1251
+ "--steps <steps>",
1252
+ "Comma list: timeline,mentions,mention-threads,likes,bookmarks,dms",
1253
+ )
1254
+ .option("--mode <mode>", "auto, xurl, or bird for likes/bookmarks", "auto")
1255
+ .option("--limit <n>", "Per-page/result limit", "100")
1256
+ .option("--max-pages <n>", "Stop after N pages", "3")
1257
+ .option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
1258
+ .option("--no-refresh", "Allow live-cache reuse")
1259
+ .option(
1260
+ "--allow-bird-account",
1261
+ "Assert bird cookies match --account for Bird-backed steps",
1262
+ )
1263
+ .option("--log <path>", "Audit JSONL path")
1264
+ .option("--env-path <path>", "Shell env file to source before running")
1265
+ .option("--env-file <path>", "Deprecated alias for --env-path")
1266
+ .option("--stdout <path>", "launchd stdout path")
1267
+ .option("--stderr <path>", "launchd stderr path")
1268
+ .option("--launch-agents-dir <path>", "LaunchAgents directory")
1269
+ .option("--no-load", "Write plist without loading it")
1270
+ .action(async (options) => {
1271
+ const result = await installAccountSyncLaunchAgent({
1272
+ label: options.label,
1273
+ intervalSeconds: Number(options.intervalSeconds),
1274
+ program: options.program,
1275
+ account: options.account,
1276
+ steps: parseAccountSyncSteps(options.steps),
1277
+ mode: options.mode as TimelineCollectionMode,
1278
+ limit: Number(options.limit),
1279
+ maxPages: Number(options.maxPages),
1280
+ refresh: options.refresh,
1281
+ allowBirdAccount: Boolean(options.allowBirdAccount),
1282
+ cacheTtlSeconds: Number(options.cacheTtl),
1283
+ logPath: options.log,
1284
+ envFile: options.envPath ?? options.envFile,
1285
+ stdoutPath: options.stdout,
1286
+ stderrPath: options.stderr,
1287
+ launchAgentsDir: options.launchAgentsDir,
1288
+ load: options.load,
1289
+ });
1290
+ print(result, true);
1291
+ });
1292
+
1036
1293
  jobsCommand
1037
1294
  .command("sync-bookmarks")
1038
1295
  .description("Refresh live bookmarks and append a JSONL audit entry")
@@ -1074,7 +1331,8 @@ jobsCommand
1074
1331
  .option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
1075
1332
  .option("--no-refresh", "Allow live-cache reuse")
1076
1333
  .option("--log <path>", "Audit JSONL path")
1077
- .option("--env-file <path>", "Shell env file to source before running")
1334
+ .option("--env-path <path>", "Shell env file to source before running")
1335
+ .option("--env-file <path>", "Deprecated alias for --env-path")
1078
1336
  .option("--stdout <path>", "launchd stdout path")
1079
1337
  .option("--stderr <path>", "launchd stderr path")
1080
1338
  .option("--launch-agents-dir <path>", "LaunchAgents directory")
@@ -1091,7 +1349,7 @@ jobsCommand
1091
1349
  refresh: options.refresh,
1092
1350
  cacheTtlSeconds: Number(options.cacheTtl),
1093
1351
  logPath: options.log,
1094
- envFile: options.envFile,
1352
+ envFile: options.envPath ?? options.envFile,
1095
1353
  stdoutPath: options.stdout,
1096
1354
  stderrPath: options.stderr,
1097
1355
  launchAgentsDir: options.launchAgentsDir,
@@ -1103,14 +1361,19 @@ jobsCommand
1103
1361
  dmsCommand
1104
1362
  .command("list")
1105
1363
  .option("--account <accountId>", "Account id")
1106
- .option("--refresh", "Refresh live DMs through bird before listing")
1364
+ .option("--mode <mode>", "auto, bird, or xurl", "bird")
1365
+ .option("--refresh", "Refresh live DMs before listing")
1107
1366
  .option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
1367
+ .option("--inbox <kind>", "all, accepted, or requests", "all")
1368
+ .option("--max-pages <n>", "Additional accepted/request pages to sync", "0")
1369
+ .option("--all-pages", "Fetch all accepted/request pages while syncing")
1370
+ .option("--page-delay-ms <n>", "Delay between live DM page requests", "0")
1108
1371
  .option("--participant <value>")
1109
1372
  .option("--min-followers <n>", "Minimum sender follower count")
1110
1373
  .option("--max-followers <n>", "Maximum sender follower count")
1111
1374
  .option("--min-influence-score <n>", "Minimum derived influence score")
1112
1375
  .option("--max-influence-score <n>", "Maximum derived influence score")
1113
- .option("--sort <mode>", "recent or influence", "recent")
1376
+ .option("--sort <mode>", "recent or followers", "recent")
1114
1377
  .option(
1115
1378
  "--resolve-profiles",
1116
1379
  "Resolve placeholder DM profiles through cache/bird/xurl",
@@ -1131,10 +1394,33 @@ dmsCommand
1131
1394
  : options.unreplied
1132
1395
  ? "unreplied"
1133
1396
  : "all";
1397
+ const inbox = parseDmInboxOption(options.inbox);
1398
+ const mode = parseDmSyncModeOption(options.mode);
1399
+ const maxPages = parseNonNegativeIntegerOption(
1400
+ options.maxPages,
1401
+ "--max-pages",
1402
+ );
1403
+ const pageDelayMs = parseNonNegativeIntegerOption(
1404
+ options.pageDelayMs,
1405
+ "--page-delay-ms",
1406
+ );
1407
+ if (
1408
+ inbox === undefined ||
1409
+ mode === undefined ||
1410
+ maxPages === undefined ||
1411
+ pageDelayMs === undefined
1412
+ ) {
1413
+ return;
1414
+ }
1134
1415
  if (options.refresh) {
1135
1416
  await syncDirectMessagesViaCachedBird({
1136
1417
  account: options.account,
1418
+ mode,
1137
1419
  limit: Number(options.limit),
1420
+ ...(inbox !== "all" ? { inbox } : {}),
1421
+ ...(maxPages > 0 ? { maxPages } : {}),
1422
+ ...(options.allPages ? { allPages: true } : {}),
1423
+ ...(pageDelayMs > 0 ? { pageDelayMs } : {}),
1138
1424
  refresh: true,
1139
1425
  cacheTtlMs: Number(options.cacheTtl) * 1000,
1140
1426
  });
@@ -1145,6 +1431,7 @@ dmsCommand
1145
1431
  const items = await enrichDmItems(
1146
1432
  {
1147
1433
  account: options.account,
1434
+ ...(inbox !== "all" ? { inbox } : {}),
1148
1435
  participant: options.participant,
1149
1436
  minFollowers: options.minFollowers
1150
1437
  ? Number(options.minFollowers)
@@ -1158,7 +1445,10 @@ dmsCommand
1158
1445
  maxInfluenceScore: options.maxInfluenceScore
1159
1446
  ? Number(options.maxInfluenceScore)
1160
1447
  : undefined,
1161
- sort: options.sort === "influence" ? "influence" : "recent",
1448
+ sort:
1449
+ options.sort === "followers" || options.sort === "influence"
1450
+ ? "followers"
1451
+ : "recent",
1162
1452
  replyFilter,
1163
1453
  limit: Number(options.limit),
1164
1454
  },
@@ -1175,15 +1465,43 @@ dmsCommand
1175
1465
 
1176
1466
  dmsCommand
1177
1467
  .command("sync")
1178
- .description("Refresh live direct messages through bird into the local store")
1468
+ .description("Refresh live direct messages into the local store")
1179
1469
  .option("--account <accountId>", "Account id")
1470
+ .option("--mode <mode>", "auto, bird, or xurl", "bird")
1180
1471
  .option("--limit <n>", "Limit messages", "20")
1472
+ .option("--inbox <kind>", "all, accepted, or requests", "all")
1473
+ .option("--max-pages <n>", "Additional accepted/request pages to sync", "0")
1474
+ .option("--all-pages", "Fetch all accepted/request pages")
1475
+ .option("--page-delay-ms <n>", "Delay between live DM page requests", "0")
1181
1476
  .option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
1182
1477
  .option("--refresh", "Bypass live-cache freshness window")
1183
1478
  .action(async (options) => {
1479
+ const inbox = parseDmInboxOption(options.inbox);
1480
+ const mode = parseDmSyncModeOption(options.mode);
1481
+ const maxPages = parseNonNegativeIntegerOption(
1482
+ options.maxPages,
1483
+ "--max-pages",
1484
+ );
1485
+ const pageDelayMs = parseNonNegativeIntegerOption(
1486
+ options.pageDelayMs,
1487
+ "--page-delay-ms",
1488
+ );
1489
+ if (
1490
+ inbox === undefined ||
1491
+ mode === undefined ||
1492
+ maxPages === undefined ||
1493
+ pageDelayMs === undefined
1494
+ ) {
1495
+ return;
1496
+ }
1184
1497
  const result = await syncDirectMessagesViaCachedBird({
1185
1498
  account: options.account,
1499
+ mode,
1186
1500
  limit: Number(options.limit),
1501
+ ...(inbox !== "all" ? { inbox } : {}),
1502
+ ...(maxPages > 0 ? { maxPages } : {}),
1503
+ ...(options.allPages ? { allPages: true } : {}),
1504
+ ...(pageDelayMs > 0 ? { pageDelayMs } : {}),
1187
1505
  refresh: Boolean(options.refresh),
1188
1506
  cacheTtlMs: Number(options.cacheTtl) * 1000,
1189
1507
  });
@@ -1191,6 +1509,39 @@ dmsCommand
1191
1509
  print(result, true);
1192
1510
  });
1193
1511
 
1512
+ for (const action of ["accept", "reject", "block"] as const) {
1513
+ const command = dmsCommand
1514
+ .command(`${action} <conversationId>`)
1515
+ .description(`${action} a live DM message request through bird`);
1516
+ if (action === "block") {
1517
+ command
1518
+ .option("--max-pages <n>", "Additional timeline pages to search", "3")
1519
+ .option("--all-pages", "Search all accepted/request timeline pages");
1520
+ }
1521
+ command.action(async (conversationId, options) => {
1522
+ const maxPages =
1523
+ action === "block"
1524
+ ? parseNonNegativeIntegerOption(options.maxPages, "--max-pages")
1525
+ : undefined;
1526
+ if (action === "block" && maxPages === undefined) {
1527
+ return;
1528
+ }
1529
+ const result = await runDirectMessageRequestMutationViaBird({
1530
+ action,
1531
+ conversationId,
1532
+ ...(action === "block" && maxPages !== undefined ? { maxPages } : {}),
1533
+ ...(action === "block" && options.allPages ? { allPages: true } : {}),
1534
+ });
1535
+ if (result.success) {
1536
+ applyDmRequestMutationToLocalStore(conversationId, action);
1537
+ } else {
1538
+ process.exitCode = 1;
1539
+ }
1540
+ await autoSyncAfterWrite();
1541
+ print(result, true);
1542
+ });
1543
+ }
1544
+
1194
1545
  registerModerationCommands({
1195
1546
  program,
1196
1547
  print,
@@ -1470,9 +1821,17 @@ program
1470
1821
  await autoUpdateBeforeRead();
1471
1822
  const child = spawn(
1472
1823
  process.execPath,
1473
- ["node_modules/vite/bin/vite.js", "dev", "--port", "3000"],
1824
+ [
1825
+ "node_modules/vite/bin/vite.js",
1826
+ "dev",
1827
+ "--host",
1828
+ "127.0.0.1",
1829
+ "--port",
1830
+ "3000",
1831
+ ],
1474
1832
  {
1475
1833
  cwd: packageRoot,
1834
+ env: { ...process.env, BIRDCLAW_LOCAL_WEB: "1" },
1476
1835
  stdio: "inherit",
1477
1836
  detached: process.platform !== "win32",
1478
1837
  },
@@ -1527,7 +1886,11 @@ program
1527
1886
  });
1528
1887
 
1529
1888
  export async function runCli(argv = process.argv) {
1530
- await program.parseAsync(argv);
1889
+ try {
1890
+ await program.parseAsync(argv);
1891
+ } finally {
1892
+ await closeDatabase();
1893
+ }
1531
1894
  }
1532
1895
 
1533
1896
  /* v8 ignore next 5 */