birdclaw 0.8.3 → 0.8.4
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/CHANGELOG.md +17 -0
- package/README.md +10 -8
- package/bin/birdclaw.mjs +4 -66
- package/dist/cli/birdclaw.js +28165 -0
- package/dist/client/assets/AvatarChip-DLoqM5xD.js +1 -0
- package/dist/client/assets/ProfilePreview-BejAuYsJ.js +1 -0
- package/dist/client/assets/SavedTimelineView-BIS7HKpp.js +1 -0
- package/dist/client/assets/SmartTimestamp-BR0-9se2.js +1 -0
- package/dist/client/assets/TimelineRouteFrame-CyGPj0eM.js +1 -0
- package/dist/client/assets/blocks-7UDKQp8H.js +1 -0
- package/dist/client/assets/bookmarks-CbBkMvXk.js +1 -0
- package/dist/client/assets/chunk-C0phanGV.js +1 -0
- package/dist/client/assets/circle-check-C_Vq3ElD.js +1 -0
- package/dist/client/assets/client-stream-contracts-ys3XhWG4.js +5 -0
- package/dist/client/assets/createLucideIcon-JV8isFFR.js +1 -0
- package/dist/client/assets/data-sources-Cm2bpNvg.js +1 -0
- package/dist/client/assets/discuss-DTjaOugw.js +1 -0
- package/dist/client/assets/dms-CGd6Eogq.js +1 -0
- package/dist/client/assets/inbox-BHlGuOy2.js +1 -0
- package/dist/client/assets/index-CEIG7S6s.js +31 -0
- package/dist/client/assets/index-bpFrQ5yG.css +1 -0
- package/dist/client/assets/likes-cLrcAQem.js +1 -0
- package/dist/client/assets/links-CM9QO1LC.js +1 -0
- package/dist/client/assets/mapbox-BxTlqXt9.js +2 -0
- package/dist/client/assets/mapbox-gl-dgf_hX3a.js +3215 -0
- package/dist/client/assets/mentions-CxKCgzIf.js +1 -0
- package/dist/client/assets/network-map-B5a3whqj.js +2 -0
- package/dist/client/assets/profile-analyze-CRtNEwlb.js +1 -0
- package/dist/client/assets/profile-hydration-client-YEtJsNTv.js +1 -0
- package/dist/client/assets/profiles._handle-C82_HoKX.js +1 -0
- package/dist/client/assets/query-client-fYu2rzRC.js +17 -0
- package/dist/client/assets/rate-limits-ScaEwdVK.js +1 -0
- package/dist/client/assets/react-dom-DHl6Ug7F.js +1 -0
- package/dist/client/assets/react-nBylq1U5.js +1 -0
- package/dist/client/assets/repeat-2-xCwHM9hD.js +1 -0
- package/dist/client/assets/routes-q7ZaW4En.js +1 -0
- package/dist/client/assets/schemas-I9r6F6ya.js +64 -0
- package/dist/client/assets/search-Dq5kYsxA.js +1 -0
- package/dist/client/assets/sparkles-CQVlvHsZ.js +1 -0
- package/dist/client/assets/styles-B8s3tt3r.css +2 -0
- package/dist/client/assets/today-CeG1O7Py.js +1 -0
- package/dist/client/assets/triangle-alert-CBL7s2pf.js +1 -0
- package/dist/client/assets/useTimelineRouteData-1HsqgvUi.js +1 -0
- package/dist/client/assets/users-CHbKlqUr.js +1 -0
- package/dist/server/assets/AvatarChip-zUDAEmjL.js +105 -0
- package/dist/server/assets/FeedState-ntW0Q2GB.js +214 -0
- package/dist/server/assets/ProfileAnalysisStream-BFvhVdhn.js +198 -0
- package/dist/server/assets/ProfilePreview-C6gAor_j.js +752 -0
- package/dist/server/assets/SavedTimelineView-BG1jnBp1.js +64 -0
- package/dist/server/assets/SmartTimestamp-DfwE1NGf.js +45 -0
- package/dist/server/assets/SyncNowButton-BOn_IBXS.js +76 -0
- package/dist/server/assets/TimelineRouteFrame-CU7ilM7U.js +85 -0
- package/dist/server/assets/TweetRichText-DhCVv1-6.js +74 -0
- package/dist/server/assets/_tanstack-start-manifest_v-C-ZRnG_n.js +148 -0
- package/dist/server/assets/account-selection-CTdFGWRk.js +44 -0
- package/dist/server/assets/blocks-C1mgGTB3.js +299 -0
- package/dist/server/assets/blocks-DSgQghqP.js +17 -0
- package/dist/server/assets/bookmarks-BJN8tykY.js +14 -0
- package/dist/server/assets/client-stream-contracts-C2xbWVjm.js +769 -0
- package/dist/server/assets/data-sources-BnA1Q94p.js +180 -0
- package/dist/server/assets/discuss-CJ1dau1j.js +15 -0
- package/dist/server/assets/discuss-QTo08NCN.js +338 -0
- package/dist/server/assets/dms-BuLj4bp8.js +368 -0
- package/dist/server/assets/dms-DAyQrVM2.js +240 -0
- package/dist/server/assets/empty-plugin-adapters-DzrEkdBF.js +5 -0
- package/dist/server/assets/inbox-CkGOaW1q.js +214 -0
- package/dist/server/assets/inbox-DpWd1EzC.js +146 -0
- package/dist/server/assets/likes-DBOAtsJU.js +14 -0
- package/dist/server/assets/links-DAhSjynu.js +240 -0
- package/dist/server/assets/links-DmH5lxHO.js +468 -0
- package/dist/server/assets/mentions-BH2MzKtB.js +26 -0
- package/dist/server/assets/network-map-D3CzRcC4.js +224 -0
- package/dist/server/assets/network-map-D5DoLOwR.js +582 -0
- package/dist/server/assets/profile-analyze-BP65PED0.js +90 -0
- package/dist/server/assets/profile-analyze-C1NoM0tV.js +9 -0
- package/dist/server/assets/profile-hydration-client-GJ4kwJS8.js +133 -0
- package/dist/server/assets/profiles._handle-DjwlN_ac.js +184 -0
- package/dist/server/assets/profiles._handle-DsLNj2Ca.js +12 -0
- package/dist/server/assets/query-client-BdauHBgd.js +923 -0
- package/dist/server/assets/rate-limits-VAr2qzYD.js +262 -0
- package/dist/server/assets/route-search-BUlQ5lGM.js +117 -0
- package/dist/server/assets/router-DqkwSPiM.js +17026 -0
- package/dist/server/assets/routes-A9c9ZhwN.js +26 -0
- package/dist/server/assets/start-FaDOTmto.js +4 -0
- package/dist/server/assets/today-B5hVM57Z.js +17 -0
- package/dist/server/assets/today-fvGiZ7ud.js +303 -0
- package/dist/server/assets/ui-qbmcMSSX.js +137 -0
- package/{src/components/useDebouncedValue.ts → dist/server/assets/useDebouncedValue-DOnA6Xcy.js} +4 -4
- package/dist/server/assets/useTimelineRouteData-27cxynxa.js +1101 -0
- package/dist/server/server.js +5520 -0
- package/package.json +26 -34
- package/playwright.config.ts +0 -35
- package/scripts/browser-perf.mjs +0 -427
- package/scripts/build-docs-site.mjs +0 -967
- package/scripts/docs-site-assets.mjs +0 -311
- package/scripts/run-vitest.mjs +0 -21
- package/scripts/sanitize-node-options.mjs +0 -23
- package/scripts/start-test-server.mjs +0 -42
- package/src/cli/command-context.ts +0 -17
- package/src/cli/register-analysis.ts +0 -500
- package/src/cli/register-compose.ts +0 -40
- package/src/cli/register-graph.ts +0 -132
- package/src/cli/register-inbox.ts +0 -41
- package/src/cli/register-storage.ts +0 -106
- package/src/cli-moderation.ts +0 -210
- package/src/cli.ts +0 -1616
- package/src/components/AccountSwitcher.tsx +0 -178
- package/src/components/AppNav.tsx +0 -118
- package/src/components/AvatarChip.tsx +0 -58
- package/src/components/AvatarPreload.ts +0 -149
- package/src/components/BrandMark.tsx +0 -67
- package/src/components/ConversationThread.tsx +0 -131
- package/src/components/DmWorkspace.tsx +0 -286
- package/src/components/EmbeddedTweetCard.tsx +0 -55
- package/src/components/FeedState.tsx +0 -147
- package/src/components/FloatingPreview.tsx +0 -382
- package/src/components/InboxCard.tsx +0 -152
- package/src/components/LinkPreviewCard.tsx +0 -292
- package/src/components/MarkdownCitations.tsx +0 -680
- package/src/components/MarkdownViewer.tsx +0 -111
- package/src/components/ProfileAnalysisClient.ts +0 -191
- package/src/components/ProfileAnalysisStream.tsx +0 -259
- package/src/components/ProfilePreview.tsx +0 -131
- package/src/components/SavedTimelineView.tsx +0 -155
- package/src/components/SmartTimestamp.tsx +0 -72
- package/src/components/SyncNowButton.tsx +0 -140
- package/src/components/ThemeSlider.tsx +0 -80
- package/src/components/TimelineCard.tsx +0 -533
- package/src/components/TimelineRouteFrame.tsx +0 -186
- package/src/components/TweetArticleCard.tsx +0 -66
- package/src/components/TweetMediaGrid.tsx +0 -185
- package/src/components/TweetRichText.tsx +0 -159
- package/src/components/account-selection.ts +0 -64
- package/src/components/links-controller.ts +0 -162
- package/src/components/links-model.ts +0 -198
- package/src/components/network-map-controller.ts +0 -84
- package/src/components/network-map-model.ts +0 -255
- package/src/components/useTimelineRouteData.ts +0 -193
- package/src/lib/account-sync-job.ts +0 -666
- package/src/lib/actions-transport.ts +0 -300
- package/src/lib/analysis-runtime.ts +0 -238
- package/src/lib/api-client.ts +0 -220
- package/src/lib/api-contracts.ts +0 -328
- package/src/lib/archive-finder.ts +0 -185
- package/src/lib/archive-import-plan.ts +0 -102
- package/src/lib/archive-import.ts +0 -2695
- package/src/lib/authored-live.ts +0 -1027
- package/src/lib/avatar-cache.ts +0 -349
- package/src/lib/backup.ts +0 -2652
- package/src/lib/bird-actions.ts +0 -294
- package/src/lib/bird-command.ts +0 -130
- package/src/lib/bird.ts +0 -1140
- package/src/lib/blocklist.ts +0 -136
- package/src/lib/blocks-write.ts +0 -171
- package/src/lib/blocks.ts +0 -387
- package/src/lib/bookmark-sync-job.ts +0 -536
- package/src/lib/config.ts +0 -224
- package/src/lib/conversation-surface.ts +0 -205
- package/src/lib/data-sources.ts +0 -219
- package/src/lib/database-metrics.ts +0 -88
- package/src/lib/database-migrations.ts +0 -34
- package/src/lib/database-schema.ts +0 -312
- package/src/lib/database-writer.ts +0 -69
- package/src/lib/db.ts +0 -889
- package/src/lib/dm-read-model.ts +0 -533
- package/src/lib/dms-live.ts +0 -870
- package/src/lib/effect-runtime.ts +0 -45
- package/src/lib/follow-graph.ts +0 -1087
- package/src/lib/geocoding.ts +0 -296
- package/src/lib/http-effect.ts +0 -222
- package/src/lib/identity-search-index.ts +0 -369
- package/src/lib/import-repository.ts +0 -138
- package/src/lib/inbox.ts +0 -242
- package/src/lib/link-index.ts +0 -641
- package/src/lib/link-insights.ts +0 -860
- package/src/lib/link-preview-metadata.ts +0 -754
- package/src/lib/live-sync-engine.ts +0 -209
- package/src/lib/live-transport-gateway.ts +0 -128
- package/src/lib/location.ts +0 -137
- package/src/lib/media-fetch.ts +0 -860
- package/src/lib/media-includes.ts +0 -165
- package/src/lib/mention-threads-live.ts +0 -830
- package/src/lib/mentions-export.ts +0 -147
- package/src/lib/mentions-live.ts +0 -1114
- package/src/lib/moderation-target.ts +0 -227
- package/src/lib/moderation-write.ts +0 -131
- package/src/lib/mutes-write.ts +0 -171
- package/src/lib/mutes.ts +0 -84
- package/src/lib/ndjson-stream.ts +0 -106
- package/src/lib/network-map.ts +0 -382
- package/src/lib/openai-response-runtime.ts +0 -251
- package/src/lib/openai.ts +0 -117
- package/src/lib/paginated-sync.ts +0 -93
- package/src/lib/period-digest.ts +0 -1364
- package/src/lib/present.ts +0 -118
- package/src/lib/profile-affiliation-hydration.ts +0 -228
- package/src/lib/profile-affiliations.ts +0 -262
- package/src/lib/profile-analysis.ts +0 -1198
- package/src/lib/profile-bio-entities.ts +0 -318
- package/src/lib/profile-history.ts +0 -164
- package/src/lib/profile-hydration.ts +0 -180
- package/src/lib/profile-replies.ts +0 -77
- package/src/lib/profile-resolver.ts +0 -650
- package/src/lib/queries.ts +0 -6
- package/src/lib/query-actions.ts +0 -437
- package/src/lib/query-client.tsx +0 -47
- package/src/lib/query-read-model-shared.ts +0 -52
- package/src/lib/query-read-models.ts +0 -5
- package/src/lib/query-resource.ts +0 -41
- package/src/lib/query-status.ts +0 -164
- package/src/lib/research.ts +0 -652
- package/src/lib/runtime-services.ts +0 -20
- package/src/lib/search-discussion.ts +0 -812
- package/src/lib/seed.ts +0 -613
- package/src/lib/server-runtime-services.ts +0 -30
- package/src/lib/sqlite.ts +0 -183
- package/src/lib/streaming-ingestion.ts +0 -240
- package/src/lib/sync-cache.ts +0 -70
- package/src/lib/sync-plan.ts +0 -175
- package/src/lib/theme-transition.ts +0 -113
- package/src/lib/theme.tsx +0 -165
- package/src/lib/timeline-collections-live.ts +0 -407
- package/src/lib/timeline-live.ts +0 -302
- package/src/lib/timeline-read-model.ts +0 -1191
- package/src/lib/tweet-account-edges.ts +0 -48
- package/src/lib/tweet-lookup.ts +0 -46
- package/src/lib/tweet-render.ts +0 -430
- package/src/lib/tweet-repository.ts +0 -156
- package/src/lib/tweet-search-live.ts +0 -462
- package/src/lib/types.ts +0 -723
- package/src/lib/ui.ts +0 -529
- package/src/lib/url-expansion-store.ts +0 -110
- package/src/lib/url-expansion.ts +0 -343
- package/src/lib/url-safety.ts +0 -220
- package/src/lib/web-sync.ts +0 -533
- package/src/lib/whois.ts +0 -1080
- package/src/lib/x-profile.ts +0 -365
- package/src/lib/x-web.ts +0 -199
- package/src/lib/xurl-rate-limits.ts +0 -267
- package/src/lib/xurl.ts +0 -1677
- package/src/routeTree.gen.ts +0 -723
- package/src/router.tsx +0 -20
- package/src/routes/__root.tsx +0 -82
- package/src/routes/api/action.tsx +0 -147
- package/src/routes/api/avatar.tsx +0 -50
- package/src/routes/api/blocks.tsx +0 -35
- package/src/routes/api/conversation.tsx +0 -45
- package/src/routes/api/data-sources.tsx +0 -24
- package/src/routes/api/inbox.tsx +0 -43
- package/src/routes/api/link-insights.tsx +0 -85
- package/src/routes/api/link-preview.tsx +0 -47
- package/src/routes/api/network-map.tsx +0 -55
- package/src/routes/api/period-digest.tsx +0 -101
- package/src/routes/api/profile-analysis.tsx +0 -97
- package/src/routes/api/profile-hydrate.tsx +0 -54
- package/src/routes/api/query.tsx +0 -121
- package/src/routes/api/search-discussion.tsx +0 -113
- package/src/routes/api/status.tsx +0 -30
- package/src/routes/api/sync.tsx +0 -108
- package/src/routes/api/xurl-rate-limits.tsx +0 -24
- package/src/routes/blocks.tsx +0 -343
- package/src/routes/bookmarks.tsx +0 -18
- package/src/routes/data-sources.tsx +0 -249
- package/src/routes/discuss.tsx +0 -419
- package/src/routes/dms.tsx +0 -415
- package/src/routes/inbox.tsx +0 -232
- package/src/routes/index.tsx +0 -32
- package/src/routes/likes.tsx +0 -18
- package/src/routes/links.tsx +0 -631
- package/src/routes/mentions.tsx +0 -32
- package/src/routes/network-map.tsx +0 -732
- package/src/routes/profile-analyze.tsx +0 -112
- package/src/routes/profiles.$handle.tsx +0 -228
- package/src/routes/rate-limits.tsx +0 -305
- package/src/routes/today.tsx +0 -511
- package/src/styles.css +0 -257
- package/tsconfig.json +0 -29
- package/vite.config.ts +0 -29
- /package/{public → dist/client}/birdclaw-mark.png +0 -0
- /package/{public → dist/client}/favicon.ico +0 -0
- /package/{public → dist/client}/logo192.png +0 -0
- /package/{public → dist/client}/logo512.png +0 -0
- /package/{public → dist/client}/manifest.json +0 -0
- /package/{public → dist/client}/robots.txt +0 -0
package/src/cli.ts
DELETED
|
@@ -1,1616 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
4
|
-
import { dirname } from "node:path";
|
|
5
|
-
import process from "node:process";
|
|
6
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
-
import { Command } from "commander";
|
|
8
|
-
import { registerModerationCommands } from "#/cli-moderation";
|
|
9
|
-
import { registerAnalysisCommands } from "#/cli/register-analysis";
|
|
10
|
-
import { registerComposeCommands } from "#/cli/register-compose";
|
|
11
|
-
import { registerGraphCommands } from "#/cli/register-graph";
|
|
12
|
-
import { registerInboxCommand } from "#/cli/register-inbox";
|
|
13
|
-
import { registerStorageCommands } from "#/cli/register-storage";
|
|
14
|
-
import { findArchives } from "#/lib/archive-finder";
|
|
15
|
-
import {
|
|
16
|
-
ARCHIVE_IMPORT_SLICES,
|
|
17
|
-
type ArchiveImportSlice,
|
|
18
|
-
type ImportProgressEvent,
|
|
19
|
-
type ImportProgressSlice,
|
|
20
|
-
type ImportWritePhase,
|
|
21
|
-
importArchive,
|
|
22
|
-
} from "#/lib/archive-import";
|
|
23
|
-
import {
|
|
24
|
-
AuthoredSyncError,
|
|
25
|
-
syncAuthoredTweets,
|
|
26
|
-
type AuthoredSyncMode,
|
|
27
|
-
} from "#/lib/authored-live";
|
|
28
|
-
import {
|
|
29
|
-
installAccountSyncLaunchAgent,
|
|
30
|
-
parseAccountSyncSteps,
|
|
31
|
-
runAccountSyncJob,
|
|
32
|
-
} from "#/lib/account-sync-job";
|
|
33
|
-
import { maybeAutoSyncBackup, maybeAutoUpdateBackup } from "#/lib/backup";
|
|
34
|
-
import {
|
|
35
|
-
installBookmarkSyncLaunchAgent,
|
|
36
|
-
runBookmarkSyncJob,
|
|
37
|
-
} from "#/lib/bookmark-sync-job";
|
|
38
|
-
import { runDirectMessageRequestMutationViaBird } from "#/lib/bird";
|
|
39
|
-
import { importBlocklist } from "#/lib/blocklist";
|
|
40
|
-
import {
|
|
41
|
-
type ActionsTransport,
|
|
42
|
-
ensureBirdclawDirs,
|
|
43
|
-
resolveMentionsDataSource,
|
|
44
|
-
setActionsTransport,
|
|
45
|
-
} from "#/lib/config";
|
|
46
|
-
import { closeDatabase } from "#/lib/db";
|
|
47
|
-
import {
|
|
48
|
-
type DirectMessagesSyncMode,
|
|
49
|
-
syncDirectMessagesViaCachedBird,
|
|
50
|
-
} from "#/lib/dms-live";
|
|
51
|
-
import { backfillLinkIndex, searchLinks } from "#/lib/link-index";
|
|
52
|
-
import { fetchTweetMedia, formatMediaFetchResult } from "#/lib/media-fetch";
|
|
53
|
-
import { syncMentionThreads } from "#/lib/mention-threads-live";
|
|
54
|
-
import { exportMentionItems } from "#/lib/mentions-export";
|
|
55
|
-
import {
|
|
56
|
-
exportMentionsViaCachedAuto,
|
|
57
|
-
exportMentionsViaCachedBird,
|
|
58
|
-
exportMentionsViaCachedXurl,
|
|
59
|
-
syncMentions,
|
|
60
|
-
} from "#/lib/mentions-live";
|
|
61
|
-
import { syncFollowGraph } from "#/lib/follow-graph";
|
|
62
|
-
import { hydrateProfilesFromX } from "#/lib/profile-hydration";
|
|
63
|
-
import { resolveProfilesForIds } from "#/lib/profile-resolver";
|
|
64
|
-
import { inspectProfileReplies } from "#/lib/profile-replies";
|
|
65
|
-
import {
|
|
66
|
-
applyDmRequestMutationToLocalStore,
|
|
67
|
-
getQueryEnvelope,
|
|
68
|
-
listDmConversations,
|
|
69
|
-
listTimelineItems,
|
|
70
|
-
} from "#/lib/queries";
|
|
71
|
-
import {
|
|
72
|
-
syncTimelineCollection,
|
|
73
|
-
type TimelineCollectionMode,
|
|
74
|
-
} from "#/lib/timeline-collections-live";
|
|
75
|
-
import { syncHomeTimeline } from "#/lib/timeline-live";
|
|
76
|
-
import { expandUrlsFromTexts } from "#/lib/url-expansion";
|
|
77
|
-
import { formatWhois, runWhois } from "#/lib/whois";
|
|
78
|
-
|
|
79
|
-
const program = new Command();
|
|
80
|
-
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
81
|
-
const packageVersion = JSON.parse(
|
|
82
|
-
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
83
|
-
) as { version?: string };
|
|
84
|
-
|
|
85
|
-
function print(data: unknown, asJson: boolean) {
|
|
86
|
-
if (asJson) {
|
|
87
|
-
console.log(JSON.stringify(data, null, 2));
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
console.log(data);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function printError(error: string) {
|
|
94
|
-
console.error(JSON.stringify({ error }));
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function errorMessage(error: unknown) {
|
|
98
|
-
return error instanceof Error ? error.message : String(error);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const IMPORT_SLICE_LABELS: Record<ImportProgressSlice, string> = {
|
|
102
|
-
tweets: "tweets",
|
|
103
|
-
noteTweets: "note tweets",
|
|
104
|
-
directMessages: "direct messages",
|
|
105
|
-
likes: "likes",
|
|
106
|
-
bookmarks: "bookmarks",
|
|
107
|
-
media: "media files",
|
|
108
|
-
followers: "followers",
|
|
109
|
-
following: "following",
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const IMPORT_WRITE_LABELS: Record<ImportWritePhase, string> = {
|
|
113
|
-
profiles: "profiles",
|
|
114
|
-
tweets: "tweets",
|
|
115
|
-
collections: "likes+bookmarks",
|
|
116
|
-
dmMessages: "DM messages",
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
function logImportProgress(event: ImportProgressEvent) {
|
|
120
|
-
switch (event.kind) {
|
|
121
|
-
case "scanned":
|
|
122
|
-
process.stderr.write(
|
|
123
|
-
`Scanning archive… ${String(event.entryCount)} entries\n`,
|
|
124
|
-
);
|
|
125
|
-
return;
|
|
126
|
-
case "slice-start":
|
|
127
|
-
if (event.slice === "media") {
|
|
128
|
-
process.stderr.write("Indexing media files…\n");
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
process.stderr.write(
|
|
132
|
-
`Parsing ${IMPORT_SLICE_LABELS[event.slice]}… (${String(event.files)} file${event.files === 1 ? "" : "s"})\n`,
|
|
133
|
-
);
|
|
134
|
-
return;
|
|
135
|
-
case "slice-file":
|
|
136
|
-
if (event.files > 1) {
|
|
137
|
-
process.stderr.write(
|
|
138
|
-
` ${IMPORT_SLICE_LABELS[event.slice]} ${String(event.processed)}/${String(event.files)}\n`,
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
return;
|
|
142
|
-
case "slice-done":
|
|
143
|
-
process.stderr.write(
|
|
144
|
-
` ${IMPORT_SLICE_LABELS[event.slice]}: ${event.count.toLocaleString()}\n`,
|
|
145
|
-
);
|
|
146
|
-
return;
|
|
147
|
-
case "writing":
|
|
148
|
-
process.stderr.write("Writing to database…\n");
|
|
149
|
-
return;
|
|
150
|
-
case "write-start":
|
|
151
|
-
process.stderr.write(
|
|
152
|
-
`Writing ${IMPORT_WRITE_LABELS[event.phase]}… (${event.total.toLocaleString()})\n`,
|
|
153
|
-
);
|
|
154
|
-
return;
|
|
155
|
-
case "write-progress":
|
|
156
|
-
process.stderr.write(
|
|
157
|
-
` ${IMPORT_WRITE_LABELS[event.phase]} ${event.processed.toLocaleString()}/${event.total.toLocaleString()}\n`,
|
|
158
|
-
);
|
|
159
|
-
return;
|
|
160
|
-
case "done":
|
|
161
|
-
process.stderr.write("Import complete.\n");
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function formatLinkSearchItems(items: ReturnType<typeof searchLinks>) {
|
|
167
|
-
return items
|
|
168
|
-
.map((item) => {
|
|
169
|
-
const linked = item.linkedTweet
|
|
170
|
-
? ` -> @${item.linkedTweet.author.handle}/${item.linkedTweet.id}: ${item.linkedTweet.text}`
|
|
171
|
-
: ` -> ${item.expansion.finalUrl}`;
|
|
172
|
-
const source =
|
|
173
|
-
item.occurrence.sourceKind === "dm"
|
|
174
|
-
? `dm ${item.occurrence.direction ?? ""}`.trim()
|
|
175
|
-
: "tweet";
|
|
176
|
-
const participant = item.participant
|
|
177
|
-
? ` @${item.participant.handle}`
|
|
178
|
-
: "";
|
|
179
|
-
return `${item.occurrence.createdAt} ${source}${participant}: ${item.occurrence.shortUrl}${linked}`;
|
|
180
|
-
})
|
|
181
|
-
.join("\n");
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function parseNonNegativeIntegerOption(
|
|
185
|
-
value: string | undefined,
|
|
186
|
-
option: string,
|
|
187
|
-
) {
|
|
188
|
-
if (value === undefined) {
|
|
189
|
-
return undefined;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const trimmed = value.trim();
|
|
193
|
-
if (!/^\d+$/.test(trimmed)) {
|
|
194
|
-
printError(`${option} must be a non-negative integer`);
|
|
195
|
-
process.exitCode = 1;
|
|
196
|
-
return undefined;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const parsed = Number.parseInt(trimmed, 10);
|
|
200
|
-
if (!Number.isSafeInteger(parsed)) {
|
|
201
|
-
printError(`${option} must be a non-negative integer`);
|
|
202
|
-
process.exitCode = 1;
|
|
203
|
-
return undefined;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return parsed;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function parsePositiveIntegerOption(value: string | undefined, option: string) {
|
|
210
|
-
const parsed = parseNonNegativeIntegerOption(value, option);
|
|
211
|
-
if (parsed === undefined) {
|
|
212
|
-
return undefined;
|
|
213
|
-
}
|
|
214
|
-
if (parsed < 1) {
|
|
215
|
-
printError(`${option} must be at least 1`);
|
|
216
|
-
process.exitCode = 1;
|
|
217
|
-
return undefined;
|
|
218
|
-
}
|
|
219
|
-
return parsed;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function parseDmInboxOption(
|
|
223
|
-
value: string | undefined,
|
|
224
|
-
): "all" | "accepted" | "requests" | undefined {
|
|
225
|
-
const normalized = (value ?? "all").trim().toLowerCase();
|
|
226
|
-
if (
|
|
227
|
-
normalized === "all" ||
|
|
228
|
-
normalized === "accepted" ||
|
|
229
|
-
normalized === "requests"
|
|
230
|
-
) {
|
|
231
|
-
return normalized;
|
|
232
|
-
}
|
|
233
|
-
if (normalized === "request") {
|
|
234
|
-
return "requests";
|
|
235
|
-
}
|
|
236
|
-
printError("--inbox must be all, accepted, or requests");
|
|
237
|
-
process.exitCode = 1;
|
|
238
|
-
return undefined;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function parseDmSyncModeOption(
|
|
242
|
-
value: string | undefined,
|
|
243
|
-
): DirectMessagesSyncMode | undefined {
|
|
244
|
-
const normalized = (value ?? "bird").trim().toLowerCase();
|
|
245
|
-
if (normalized === "auto" || normalized === "bird" || normalized === "xurl") {
|
|
246
|
-
return normalized;
|
|
247
|
-
}
|
|
248
|
-
printError("--mode must be auto, bird, or xurl");
|
|
249
|
-
process.exitCode = 1;
|
|
250
|
-
return undefined;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function parseArchiveImportSelect(value: string | undefined) {
|
|
254
|
-
if (value === undefined) {
|
|
255
|
-
return undefined;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const aliases: Record<string, ArchiveImportSlice> = Object.assign(
|
|
259
|
-
Object.create(null) as Record<string, ArchiveImportSlice>,
|
|
260
|
-
{
|
|
261
|
-
tweets: "tweets",
|
|
262
|
-
likes: "likes",
|
|
263
|
-
bookmarks: "bookmarks",
|
|
264
|
-
directmessages: "directMessages",
|
|
265
|
-
"direct-messages": "directMessages",
|
|
266
|
-
dms: "directMessages",
|
|
267
|
-
profiles: "profiles",
|
|
268
|
-
followers: "followers",
|
|
269
|
-
following: "following",
|
|
270
|
-
},
|
|
271
|
-
);
|
|
272
|
-
const selected: ArchiveImportSlice[] = [];
|
|
273
|
-
const seen = new Set<ArchiveImportSlice>();
|
|
274
|
-
for (const rawItem of value.split(",")) {
|
|
275
|
-
const item = rawItem.trim();
|
|
276
|
-
if (!item) continue;
|
|
277
|
-
const slice = aliases[item] ?? aliases[item.toLowerCase()];
|
|
278
|
-
if (!slice) {
|
|
279
|
-
printError(
|
|
280
|
-
`--select must be a comma-separated subset of ${ARCHIVE_IMPORT_SLICES.join(", ")}`,
|
|
281
|
-
);
|
|
282
|
-
process.exitCode = 1;
|
|
283
|
-
return undefined;
|
|
284
|
-
}
|
|
285
|
-
if (!seen.has(slice)) {
|
|
286
|
-
seen.add(slice);
|
|
287
|
-
selected.push(slice);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
if (selected.length === 0) {
|
|
292
|
-
printError(
|
|
293
|
-
`--select must include at least one of ${ARCHIVE_IMPORT_SLICES.join(", ")}`,
|
|
294
|
-
);
|
|
295
|
-
process.exitCode = 1;
|
|
296
|
-
return undefined;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
return selected;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function resolveActionOptions(options: { transport?: string }) {
|
|
303
|
-
return {
|
|
304
|
-
transport: options.transport as ActionsTransport | undefined,
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function parseActionsTransport(value: string | undefined) {
|
|
309
|
-
const normalized = value?.trim().toLowerCase();
|
|
310
|
-
if (normalized === "auto" || normalized === "bird" || normalized === "xurl") {
|
|
311
|
-
return normalized;
|
|
312
|
-
}
|
|
313
|
-
printError("transport must be auto, bird, or xurl");
|
|
314
|
-
process.exitCode = 1;
|
|
315
|
-
return undefined;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
async function enrichDmItems(
|
|
319
|
-
query: Parameters<typeof listDmConversations>[0],
|
|
320
|
-
options: {
|
|
321
|
-
resolveProfiles?: boolean;
|
|
322
|
-
expandUrls?: boolean;
|
|
323
|
-
refreshProfileCache?: boolean;
|
|
324
|
-
refreshUrlCache?: boolean;
|
|
325
|
-
xurlFallback?: boolean;
|
|
326
|
-
},
|
|
327
|
-
) {
|
|
328
|
-
let items = listDmConversations(query);
|
|
329
|
-
const profileResolution = options.resolveProfiles
|
|
330
|
-
? await resolveProfilesForIds(
|
|
331
|
-
items.map((item) => item.participant.id),
|
|
332
|
-
{
|
|
333
|
-
refresh: options.refreshProfileCache,
|
|
334
|
-
xurlFallback: options.xurlFallback ?? true,
|
|
335
|
-
},
|
|
336
|
-
)
|
|
337
|
-
: undefined;
|
|
338
|
-
if (profileResolution) {
|
|
339
|
-
items = listDmConversations(query);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const urlExpansions = options.expandUrls
|
|
343
|
-
? await expandUrlsFromTexts(
|
|
344
|
-
items.flatMap((item) => [
|
|
345
|
-
item.lastMessagePreview,
|
|
346
|
-
item.searchSnippet ?? "",
|
|
347
|
-
...(item.matches ?? []).flatMap((match) => [
|
|
348
|
-
...match.before.map((message) => message.text),
|
|
349
|
-
match.message.text,
|
|
350
|
-
...match.after.map((message) => message.text),
|
|
351
|
-
]),
|
|
352
|
-
]),
|
|
353
|
-
{ refresh: options.refreshUrlCache },
|
|
354
|
-
)
|
|
355
|
-
: undefined;
|
|
356
|
-
|
|
357
|
-
if (!profileResolution && !urlExpansions) {
|
|
358
|
-
return items;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
return {
|
|
362
|
-
items,
|
|
363
|
-
...(profileResolution ? { profileResolution } : {}),
|
|
364
|
-
...(urlExpansions ? { urlExpansions } : {}),
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
async function autoUpdateBeforeRead() {
|
|
369
|
-
let result: Awaited<ReturnType<typeof maybeAutoUpdateBackup>>;
|
|
370
|
-
try {
|
|
371
|
-
result = await maybeAutoUpdateBackup();
|
|
372
|
-
} catch (error) {
|
|
373
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
374
|
-
console.error(`birdclaw backup auto-sync failed: ${message}`);
|
|
375
|
-
return;
|
|
376
|
-
}
|
|
377
|
-
if (!result.ok) {
|
|
378
|
-
console.error(`birdclaw backup auto-sync failed: ${result.error}`);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
async function autoSyncAfterWrite() {
|
|
383
|
-
let result: Awaited<ReturnType<typeof maybeAutoSyncBackup>>;
|
|
384
|
-
try {
|
|
385
|
-
result = await maybeAutoSyncBackup();
|
|
386
|
-
} catch (error) {
|
|
387
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
388
|
-
console.error(`birdclaw backup sync failed: ${message}`);
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
if (!result.ok) {
|
|
392
|
-
console.error(`birdclaw backup sync failed: ${result.error}`);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
program
|
|
397
|
-
.name("birdclaw")
|
|
398
|
-
.description("Local-first Twitter workspace")
|
|
399
|
-
.version(packageVersion.version ?? "0.0.0")
|
|
400
|
-
.option("--json", "Emit JSON output");
|
|
401
|
-
|
|
402
|
-
program
|
|
403
|
-
.command("init")
|
|
404
|
-
.description("Create local birdclaw root and seed the database")
|
|
405
|
-
.action(async () => {
|
|
406
|
-
const paths = ensureBirdclawDirs();
|
|
407
|
-
await getQueryEnvelope();
|
|
408
|
-
print(
|
|
409
|
-
{
|
|
410
|
-
ok: true,
|
|
411
|
-
rootDir: paths.rootDir,
|
|
412
|
-
configPath: paths.configPath,
|
|
413
|
-
dbPath: paths.dbPath,
|
|
414
|
-
mediaOriginalsDir: paths.mediaOriginalsDir,
|
|
415
|
-
mediaThumbsDir: paths.mediaThumbsDir,
|
|
416
|
-
},
|
|
417
|
-
program.opts().json ?? false,
|
|
418
|
-
);
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
const authCommand = program
|
|
422
|
-
.command("auth")
|
|
423
|
-
.description("Manage live transport");
|
|
424
|
-
|
|
425
|
-
authCommand
|
|
426
|
-
.command("status")
|
|
427
|
-
.description("Show transport status")
|
|
428
|
-
.action(async () => {
|
|
429
|
-
const meta = await getQueryEnvelope();
|
|
430
|
-
print(meta.transport, program.opts().json ?? false);
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
authCommand
|
|
434
|
-
.command("use <transport>")
|
|
435
|
-
.description("Set preferred moderation action transport")
|
|
436
|
-
.action((transport: string) => {
|
|
437
|
-
const parsed = parseActionsTransport(transport);
|
|
438
|
-
if (!parsed) return;
|
|
439
|
-
print(setActionsTransport(parsed), program.opts().json ?? false);
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
program
|
|
443
|
-
.command("archive find")
|
|
444
|
-
.description("Find likely Twitter archives on disk")
|
|
445
|
-
.action(async () => {
|
|
446
|
-
const items = await findArchives();
|
|
447
|
-
print(items, program.opts().json ?? false);
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
const importCommand = program
|
|
451
|
-
.command("import")
|
|
452
|
-
.description("Import local archive data");
|
|
453
|
-
|
|
454
|
-
importCommand
|
|
455
|
-
.command("archive [archivePath]")
|
|
456
|
-
.description("Import a Twitter archive into the local SQLite store")
|
|
457
|
-
.option(
|
|
458
|
-
"--select <kinds>",
|
|
459
|
-
`Import only selected archive slices: ${ARCHIVE_IMPORT_SLICES.join(", ")}`,
|
|
460
|
-
)
|
|
461
|
-
.action(async (archivePath, options: { select?: string }) => {
|
|
462
|
-
const select = parseArchiveImportSelect(options.select);
|
|
463
|
-
if (options.select !== undefined && !select) {
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
let resolvedArchivePath = archivePath;
|
|
467
|
-
if (!resolvedArchivePath) {
|
|
468
|
-
const [latestArchive] = await findArchives();
|
|
469
|
-
resolvedArchivePath = latestArchive?.path;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
if (!resolvedArchivePath) {
|
|
473
|
-
throw new Error(
|
|
474
|
-
"No archive found. Pass a path or place one in Downloads.",
|
|
475
|
-
);
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
const asJson = Boolean(program.opts().json);
|
|
479
|
-
const result = await importArchive(resolvedArchivePath, {
|
|
480
|
-
select,
|
|
481
|
-
onProgress: asJson ? undefined : logImportProgress,
|
|
482
|
-
});
|
|
483
|
-
await autoSyncAfterWrite();
|
|
484
|
-
print(result, asJson);
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
importCommand
|
|
488
|
-
.command("hydrate-profiles")
|
|
489
|
-
.description("Backfill archive-imported profiles from live Twitter metadata")
|
|
490
|
-
.action(async () => {
|
|
491
|
-
const result = await hydrateProfilesFromX();
|
|
492
|
-
await autoSyncAfterWrite();
|
|
493
|
-
print(result, program.opts().json ?? false);
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
const searchCommand = program
|
|
497
|
-
.command("search")
|
|
498
|
-
.description("Search local data");
|
|
499
|
-
|
|
500
|
-
searchCommand
|
|
501
|
-
.command("tweets [query]")
|
|
502
|
-
.option("--resource <resource>", "home, mentions, or authored", "home")
|
|
503
|
-
.option("--replied", "Only replied items")
|
|
504
|
-
.option("--unreplied", "Only unreplied items")
|
|
505
|
-
.option("--since <date>", "Include tweets created at or after this date")
|
|
506
|
-
.option("--until <date>", "Include tweets created before this date")
|
|
507
|
-
.option("--originals-only", "Exclude authored replies that start with @")
|
|
508
|
-
.option("--hide-low-quality", "Hide RTs, tiny replies, and link-only noise")
|
|
509
|
-
.option(
|
|
510
|
-
"--min-likes <n>",
|
|
511
|
-
"Override the low-quality like threshold (default 50)",
|
|
512
|
-
)
|
|
513
|
-
.option("--quality-reason", "Include qualityReason on each row")
|
|
514
|
-
.option("--liked", "Only liked tweets")
|
|
515
|
-
.option("--bookmarked", "Only bookmarked tweets")
|
|
516
|
-
.option("--limit <n>", "Limit results", "20")
|
|
517
|
-
.action(async (query, options) => {
|
|
518
|
-
const minLikes = parseNonNegativeIntegerOption(
|
|
519
|
-
options.minLikes,
|
|
520
|
-
"--min-likes",
|
|
521
|
-
);
|
|
522
|
-
if (options.minLikes !== undefined && minLikes === undefined) {
|
|
523
|
-
return;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
await autoUpdateBeforeRead();
|
|
527
|
-
const replyFilter = options.replied
|
|
528
|
-
? "replied"
|
|
529
|
-
: options.unreplied
|
|
530
|
-
? "unreplied"
|
|
531
|
-
: "all";
|
|
532
|
-
const items = listTimelineItems({
|
|
533
|
-
resource:
|
|
534
|
-
options.resource === "mentions"
|
|
535
|
-
? "mentions"
|
|
536
|
-
: options.resource === "authored"
|
|
537
|
-
? "authored"
|
|
538
|
-
: "home",
|
|
539
|
-
search: query,
|
|
540
|
-
replyFilter,
|
|
541
|
-
since: options.since,
|
|
542
|
-
until: options.until,
|
|
543
|
-
includeReplies: !options.originalsOnly,
|
|
544
|
-
qualityFilter: options.hideLowQuality ? "summary" : "all",
|
|
545
|
-
lowQualityThreshold: minLikes,
|
|
546
|
-
includeQualityReason: Boolean(options.qualityReason),
|
|
547
|
-
likedOnly: Boolean(options.liked),
|
|
548
|
-
bookmarkedOnly: Boolean(options.bookmarked),
|
|
549
|
-
limit: Number(options.limit),
|
|
550
|
-
});
|
|
551
|
-
print(items, program.opts().json ?? false);
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
searchCommand
|
|
555
|
-
.command("dms <query>")
|
|
556
|
-
.option("--inbox <kind>", "all, accepted, or requests", "all")
|
|
557
|
-
.option("--participant <value>")
|
|
558
|
-
.option("--min-followers <n>", "Minimum sender follower count")
|
|
559
|
-
.option("--max-followers <n>", "Maximum sender follower count")
|
|
560
|
-
.option("--min-influence-score <n>", "Minimum derived influence score")
|
|
561
|
-
.option("--max-influence-score <n>", "Maximum derived influence score")
|
|
562
|
-
.option("--sort <mode>", "recent or followers", "recent")
|
|
563
|
-
.option(
|
|
564
|
-
"--context <n>",
|
|
565
|
-
"Include N messages before and after each match",
|
|
566
|
-
"0",
|
|
567
|
-
)
|
|
568
|
-
.option(
|
|
569
|
-
"--resolve-profiles",
|
|
570
|
-
"Resolve placeholder DM profiles through cache/bird/xurl",
|
|
571
|
-
)
|
|
572
|
-
.option("--expand-urls", "Expand URLs through the persistent URL cache")
|
|
573
|
-
.option("--refresh-profile-cache", "Bypass profile lookup cache")
|
|
574
|
-
.option("--refresh-url-cache", "Bypass URL expansion cache")
|
|
575
|
-
.option(
|
|
576
|
-
"--no-xurl-fallback",
|
|
577
|
-
"Do not fall back to xurl after bird profile lookup",
|
|
578
|
-
)
|
|
579
|
-
.option("--replied", "Only replied threads")
|
|
580
|
-
.option("--unreplied", "Only unreplied threads")
|
|
581
|
-
.option("--limit <n>", "Limit results", "20")
|
|
582
|
-
.action(async (query, options) => {
|
|
583
|
-
await autoUpdateBeforeRead();
|
|
584
|
-
const context = parseNonNegativeIntegerOption(options.context, "--context");
|
|
585
|
-
if (context === undefined) {
|
|
586
|
-
return;
|
|
587
|
-
}
|
|
588
|
-
const inbox = parseDmInboxOption(options.inbox);
|
|
589
|
-
if (inbox === undefined) {
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
const replyFilter = options.replied
|
|
593
|
-
? "replied"
|
|
594
|
-
: options.unreplied
|
|
595
|
-
? "unreplied"
|
|
596
|
-
: "all";
|
|
597
|
-
const dmQuery = {
|
|
598
|
-
search: query,
|
|
599
|
-
...(inbox !== "all" ? { inbox } : {}),
|
|
600
|
-
participant: options.participant,
|
|
601
|
-
minFollowers: options.minFollowers
|
|
602
|
-
? Number(options.minFollowers)
|
|
603
|
-
: undefined,
|
|
604
|
-
maxFollowers: options.maxFollowers
|
|
605
|
-
? Number(options.maxFollowers)
|
|
606
|
-
: undefined,
|
|
607
|
-
minInfluenceScore: options.minInfluenceScore
|
|
608
|
-
? Number(options.minInfluenceScore)
|
|
609
|
-
: undefined,
|
|
610
|
-
maxInfluenceScore: options.maxInfluenceScore
|
|
611
|
-
? Number(options.maxInfluenceScore)
|
|
612
|
-
: undefined,
|
|
613
|
-
sort:
|
|
614
|
-
options.sort === "followers" || options.sort === "influence"
|
|
615
|
-
? "followers"
|
|
616
|
-
: "recent",
|
|
617
|
-
replyFilter,
|
|
618
|
-
context,
|
|
619
|
-
limit: Number(options.limit),
|
|
620
|
-
} as const;
|
|
621
|
-
const items = await enrichDmItems(dmQuery, {
|
|
622
|
-
resolveProfiles: Boolean(options.resolveProfiles),
|
|
623
|
-
expandUrls: Boolean(options.expandUrls),
|
|
624
|
-
refreshProfileCache: Boolean(options.refreshProfileCache),
|
|
625
|
-
refreshUrlCache: Boolean(options.refreshUrlCache),
|
|
626
|
-
xurlFallback: options.xurlFallback,
|
|
627
|
-
});
|
|
628
|
-
print(items, program.opts().json ?? false);
|
|
629
|
-
});
|
|
630
|
-
|
|
631
|
-
searchCommand
|
|
632
|
-
.command("links <query>")
|
|
633
|
-
.description("Search indexed short links, expansions, and linked tweets")
|
|
634
|
-
.option("--account <accountIdOrHandle>", "Account id or handle")
|
|
635
|
-
.option("--since <date>", "Include links created at or after this date")
|
|
636
|
-
.option("--until <date>", "Include links created before this date")
|
|
637
|
-
.option("--source <kind>", "dm or tweet")
|
|
638
|
-
.option("--direction <direction>", "inbound or outbound")
|
|
639
|
-
.option("--participant <value>", "DM participant handle or name")
|
|
640
|
-
.option("--media <type>", "image, video, or gif")
|
|
641
|
-
.option("--limit <n>", "Limit results", "20")
|
|
642
|
-
.action(async (query, options) => {
|
|
643
|
-
await autoUpdateBeforeRead();
|
|
644
|
-
const items = searchLinks(query, {
|
|
645
|
-
account: options.account,
|
|
646
|
-
since: options.since,
|
|
647
|
-
until: options.until,
|
|
648
|
-
source:
|
|
649
|
-
options.source === "tweet"
|
|
650
|
-
? "tweet"
|
|
651
|
-
: options.source === "dm"
|
|
652
|
-
? "dm"
|
|
653
|
-
: undefined,
|
|
654
|
-
direction:
|
|
655
|
-
options.direction === "inbound"
|
|
656
|
-
? "inbound"
|
|
657
|
-
: options.direction === "outbound"
|
|
658
|
-
? "outbound"
|
|
659
|
-
: undefined,
|
|
660
|
-
participant: options.participant,
|
|
661
|
-
mediaType:
|
|
662
|
-
options.media === "image" ||
|
|
663
|
-
options.media === "video" ||
|
|
664
|
-
options.media === "gif"
|
|
665
|
-
? options.media
|
|
666
|
-
: undefined,
|
|
667
|
-
limit: Number(options.limit),
|
|
668
|
-
});
|
|
669
|
-
if (program.opts().json) {
|
|
670
|
-
print(items, true);
|
|
671
|
-
return;
|
|
672
|
-
}
|
|
673
|
-
console.log(formatLinkSearchItems(items));
|
|
674
|
-
});
|
|
675
|
-
|
|
676
|
-
const linksCommand = program
|
|
677
|
-
.command("links")
|
|
678
|
-
.description("Build and inspect the short-link index");
|
|
679
|
-
|
|
680
|
-
linksCommand
|
|
681
|
-
.command("backfill")
|
|
682
|
-
.description("Backfill indexed URL occurrences and t.co expansions")
|
|
683
|
-
.option("--all-urls", "Index all URLs, not only t.co")
|
|
684
|
-
.option("--source <kind>", "dm or tweet")
|
|
685
|
-
.option("--refresh-url-cache", "Re-expand URLs already in the index")
|
|
686
|
-
.option("--limit <n>", "Limit network/cache expansions for this run")
|
|
687
|
-
.option("--concurrency <n>", "Concurrent URL expansion workers", "12")
|
|
688
|
-
.option("--timeout-ms <n>", "Per-redirect fetch timeout", "15000")
|
|
689
|
-
.action(async (options) => {
|
|
690
|
-
const limit = parseNonNegativeIntegerOption(options.limit, "--limit");
|
|
691
|
-
if (options.limit !== undefined && limit === undefined) {
|
|
692
|
-
return;
|
|
693
|
-
}
|
|
694
|
-
const concurrency = parseNonNegativeIntegerOption(
|
|
695
|
-
options.concurrency,
|
|
696
|
-
"--concurrency",
|
|
697
|
-
);
|
|
698
|
-
if (concurrency === undefined) {
|
|
699
|
-
return;
|
|
700
|
-
}
|
|
701
|
-
const timeoutMs = parseNonNegativeIntegerOption(
|
|
702
|
-
options.timeoutMs,
|
|
703
|
-
"--timeout-ms",
|
|
704
|
-
);
|
|
705
|
-
if (timeoutMs === undefined) {
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
708
|
-
const result = await backfillLinkIndex({
|
|
709
|
-
includeAllUrls: Boolean(options.allUrls),
|
|
710
|
-
refresh: Boolean(options.refreshUrlCache),
|
|
711
|
-
source:
|
|
712
|
-
options.source === "tweet"
|
|
713
|
-
? "tweet"
|
|
714
|
-
: options.source === "dm"
|
|
715
|
-
? "dm"
|
|
716
|
-
: undefined,
|
|
717
|
-
limit,
|
|
718
|
-
concurrency,
|
|
719
|
-
timeoutMs,
|
|
720
|
-
});
|
|
721
|
-
await autoSyncAfterWrite();
|
|
722
|
-
print(result, program.opts().json ?? false);
|
|
723
|
-
});
|
|
724
|
-
|
|
725
|
-
const mediaCommand = program
|
|
726
|
-
.command("media")
|
|
727
|
-
.description("Manage the local media cache");
|
|
728
|
-
|
|
729
|
-
mediaCommand
|
|
730
|
-
.command("fetch")
|
|
731
|
-
.description(
|
|
732
|
-
"Fetch missing pbs.twimg.com image media already stored in tweets",
|
|
733
|
-
)
|
|
734
|
-
.option("--account <accountId>", "Account id")
|
|
735
|
-
.option("--limit <n>", "Stop after N tweets processed")
|
|
736
|
-
.option(
|
|
737
|
-
"--kind <kind>",
|
|
738
|
-
"Tweet or collection kind, e.g. home, like, bookmark",
|
|
739
|
-
)
|
|
740
|
-
.option("--since <isoDate>", "Only tweets created at or after this date")
|
|
741
|
-
.option("--parallel <n>", "Concurrent fetch workers, capped at 5", "1")
|
|
742
|
-
.option("--pacing-ms <n>", "Delay between request starts", "250")
|
|
743
|
-
.option("--video-pacing-ms <n>", "Delay between video request starts")
|
|
744
|
-
.option("--retry-max <n>", "Retries per file after rate limiting", "3")
|
|
745
|
-
.option("--include-video", "Include video and animated GIF media", true)
|
|
746
|
-
.option("--no-include-video", "Skip video and animated GIF media")
|
|
747
|
-
.option("--max-bytes <n>", "Maximum media file size in bytes", "104857600")
|
|
748
|
-
.option("--dry-run", "List what would be fetched without downloading")
|
|
749
|
-
.option("--json", "Emit JSON output")
|
|
750
|
-
.action(async (options) => {
|
|
751
|
-
const limit = parseNonNegativeIntegerOption(options.limit, "--limit");
|
|
752
|
-
if (options.limit !== undefined && limit === undefined) {
|
|
753
|
-
return;
|
|
754
|
-
}
|
|
755
|
-
const parallel =
|
|
756
|
-
parsePositiveIntegerOption(options.parallel, "--parallel") ?? 1;
|
|
757
|
-
const pacingMs =
|
|
758
|
-
parseNonNegativeIntegerOption(options.pacingMs, "--pacing-ms") ?? 250;
|
|
759
|
-
const retryMax =
|
|
760
|
-
parseNonNegativeIntegerOption(options.retryMax, "--retry-max") ?? 3;
|
|
761
|
-
const videoPacingMs =
|
|
762
|
-
options.videoPacingMs === undefined
|
|
763
|
-
? undefined
|
|
764
|
-
: parseNonNegativeIntegerOption(
|
|
765
|
-
options.videoPacingMs,
|
|
766
|
-
"--video-pacing-ms",
|
|
767
|
-
);
|
|
768
|
-
const maxBytes =
|
|
769
|
-
parseNonNegativeIntegerOption(options.maxBytes, "--max-bytes") ??
|
|
770
|
-
100 * 1024 * 1024;
|
|
771
|
-
if (process.exitCode) {
|
|
772
|
-
return;
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
const result = await fetchTweetMedia({
|
|
776
|
-
account: options.account,
|
|
777
|
-
limit,
|
|
778
|
-
kind: options.kind,
|
|
779
|
-
since: options.since,
|
|
780
|
-
parallel,
|
|
781
|
-
pacingMs,
|
|
782
|
-
videoPacingMs,
|
|
783
|
-
retryMax,
|
|
784
|
-
includeVideo: Boolean(options.includeVideo),
|
|
785
|
-
maxBytes,
|
|
786
|
-
dryRun: Boolean(options.dryRun),
|
|
787
|
-
});
|
|
788
|
-
const asJson = Boolean(program.opts().json || options.json);
|
|
789
|
-
print(asJson ? result : formatMediaFetchResult(result), asJson);
|
|
790
|
-
});
|
|
791
|
-
|
|
792
|
-
program
|
|
793
|
-
.command("whois <query>")
|
|
794
|
-
.description("Identify likely people or orgs from local DMs and tweets")
|
|
795
|
-
.option("--account <accountId>", "Account id")
|
|
796
|
-
.option("--no-dms", "Do not search DMs")
|
|
797
|
-
.option("--tweets", "Include local tweet search evidence")
|
|
798
|
-
.option("--no-resolve-profiles", "Do not resolve placeholder profiles")
|
|
799
|
-
.option("--no-expand-urls", "Do not expand URLs")
|
|
800
|
-
.option("--refresh-profile-cache", "Bypass profile lookup cache")
|
|
801
|
-
.option("--refresh-url-cache", "Bypass URL expansion cache")
|
|
802
|
-
.option(
|
|
803
|
-
"--no-xurl-fallback",
|
|
804
|
-
"Do not fall back to xurl after bird profile lookup",
|
|
805
|
-
)
|
|
806
|
-
.option(
|
|
807
|
-
"--affiliation <query>",
|
|
808
|
-
"Require affiliation, bio, or history evidence",
|
|
809
|
-
)
|
|
810
|
-
.option(
|
|
811
|
-
"--current-affiliation <query>",
|
|
812
|
-
"Require an active affiliation badge",
|
|
813
|
-
)
|
|
814
|
-
.option(
|
|
815
|
-
"--exclude-domain-only",
|
|
816
|
-
"Drop candidates that only match domains/URLs",
|
|
817
|
-
)
|
|
818
|
-
.option("--context <n>", "DM messages before and after each match", "4")
|
|
819
|
-
.option("--limit <n>", "Limit candidates", "10")
|
|
820
|
-
.action(async (query, options) => {
|
|
821
|
-
await autoUpdateBeforeRead();
|
|
822
|
-
const context = parseNonNegativeIntegerOption(options.context, "--context");
|
|
823
|
-
if (context === undefined) {
|
|
824
|
-
return;
|
|
825
|
-
}
|
|
826
|
-
const result = await runWhois(query, {
|
|
827
|
-
account: options.account,
|
|
828
|
-
dms: options.dms,
|
|
829
|
-
tweets: Boolean(options.tweets),
|
|
830
|
-
resolveProfiles: options.resolveProfiles,
|
|
831
|
-
expandUrls: options.expandUrls,
|
|
832
|
-
refreshProfileCache: Boolean(options.refreshProfileCache),
|
|
833
|
-
refreshUrlCache: Boolean(options.refreshUrlCache),
|
|
834
|
-
xurlFallback: options.xurlFallback,
|
|
835
|
-
affiliation: options.affiliation,
|
|
836
|
-
currentAffiliation: options.currentAffiliation,
|
|
837
|
-
excludeDomainOnly: Boolean(options.excludeDomainOnly),
|
|
838
|
-
context,
|
|
839
|
-
limit: Number(options.limit),
|
|
840
|
-
});
|
|
841
|
-
print(
|
|
842
|
-
program.opts().json ? result : formatWhois(result),
|
|
843
|
-
program.opts().json ?? false,
|
|
844
|
-
);
|
|
845
|
-
});
|
|
846
|
-
|
|
847
|
-
registerAnalysisCommands({
|
|
848
|
-
program,
|
|
849
|
-
print,
|
|
850
|
-
asJson: () => program.opts().json ?? false,
|
|
851
|
-
autoSyncAfterWrite,
|
|
852
|
-
autoUpdateBeforeRead,
|
|
853
|
-
parseNonNegativeIntegerOption,
|
|
854
|
-
parsePositiveIntegerOption,
|
|
855
|
-
});
|
|
856
|
-
|
|
857
|
-
const mentionsCommand = program
|
|
858
|
-
.command("mentions")
|
|
859
|
-
.description("Export local mention tweets for scripts and agents");
|
|
860
|
-
|
|
861
|
-
mentionsCommand
|
|
862
|
-
.command("export [query]")
|
|
863
|
-
.description("Return mention tweets with plain-text and markdown renderings")
|
|
864
|
-
.option("--account <accountId>", "Account id")
|
|
865
|
-
.option("--mode <mode>", "birdclaw, auto, xurl, or bird")
|
|
866
|
-
.option("--replied", "Only replied items")
|
|
867
|
-
.option("--unreplied", "Only unreplied items")
|
|
868
|
-
.option("--refresh", "Refresh the live xurl cache before returning")
|
|
869
|
-
.option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
|
|
870
|
-
.option("--all", "Fetch every retrievable xurl mentions page")
|
|
871
|
-
.option(
|
|
872
|
-
"--max-pages <n>",
|
|
873
|
-
"Maximum xurl mention pages to fetch (implies --all)",
|
|
874
|
-
)
|
|
875
|
-
.option("--limit <n>", "Limit results", "20")
|
|
876
|
-
.action(async (query, options) => {
|
|
877
|
-
await autoUpdateBeforeRead();
|
|
878
|
-
const replyFilter = options.replied
|
|
879
|
-
? "replied"
|
|
880
|
-
: options.unreplied
|
|
881
|
-
? "unreplied"
|
|
882
|
-
: "all";
|
|
883
|
-
const limit = Number(options.limit);
|
|
884
|
-
const mode = resolveMentionsDataSource(options.mode);
|
|
885
|
-
if (mode === "xurl" || mode === "bird" || mode === "auto") {
|
|
886
|
-
const exportFn =
|
|
887
|
-
mode === "xurl"
|
|
888
|
-
? exportMentionsViaCachedXurl
|
|
889
|
-
: mode === "bird"
|
|
890
|
-
? exportMentionsViaCachedBird
|
|
891
|
-
: exportMentionsViaCachedAuto;
|
|
892
|
-
const payload = await exportFn({
|
|
893
|
-
account: options.account,
|
|
894
|
-
search: query,
|
|
895
|
-
replyFilter,
|
|
896
|
-
limit,
|
|
897
|
-
all: Boolean(options.all) || options.maxPages !== undefined,
|
|
898
|
-
maxPages: options.maxPages ? Number(options.maxPages) : undefined,
|
|
899
|
-
refresh: Boolean(options.refresh),
|
|
900
|
-
cacheTtlMs: Number(options.cacheTtl) * 1000,
|
|
901
|
-
});
|
|
902
|
-
await autoSyncAfterWrite();
|
|
903
|
-
print(payload, true);
|
|
904
|
-
return;
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
const items = exportMentionItems({
|
|
908
|
-
account: options.account,
|
|
909
|
-
search: query,
|
|
910
|
-
replyFilter,
|
|
911
|
-
limit,
|
|
912
|
-
});
|
|
913
|
-
print({ resource: "mentions", count: items.length, items }, true);
|
|
914
|
-
});
|
|
915
|
-
|
|
916
|
-
const profilesCommand = program
|
|
917
|
-
.command("profiles")
|
|
918
|
-
.description("Inspect live profile context for moderation and triage");
|
|
919
|
-
|
|
920
|
-
profilesCommand
|
|
921
|
-
.command("replies <query>")
|
|
922
|
-
.description("Inspect recent authored replies for one profile")
|
|
923
|
-
.option("--limit <n>", "Limit replies", "12")
|
|
924
|
-
.action(async (query, options) => {
|
|
925
|
-
const result = await inspectProfileReplies(query, {
|
|
926
|
-
limit: Number(options.limit),
|
|
927
|
-
});
|
|
928
|
-
print(result, program.opts().json ?? false);
|
|
929
|
-
});
|
|
930
|
-
|
|
931
|
-
const dmsCommand = program.command("dms").description("Direct messages");
|
|
932
|
-
|
|
933
|
-
const syncCommand = program
|
|
934
|
-
.command("sync")
|
|
935
|
-
.description("Refresh live Twitter collections into the local store");
|
|
936
|
-
|
|
937
|
-
syncCommand
|
|
938
|
-
.command("timeline")
|
|
939
|
-
.description("Refresh live home timeline through xurl or bird")
|
|
940
|
-
.option("--account <accountId>", "Account id")
|
|
941
|
-
.option("--mode <mode>", "auto, xurl, or bird", "auto")
|
|
942
|
-
.option("--limit <n>", "Result limit", "100")
|
|
943
|
-
.option("--max-pages <n>", "Stop after N xurl pages", "3")
|
|
944
|
-
.option("--for-you", 'Fetch "For You" instead of chronological Following')
|
|
945
|
-
.option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
|
|
946
|
-
.option("--refresh", "Bypass live-cache freshness window")
|
|
947
|
-
.action(async (options) => {
|
|
948
|
-
const result = await syncHomeTimeline({
|
|
949
|
-
account: options.account,
|
|
950
|
-
mode: options.mode,
|
|
951
|
-
limit: Number(options.limit),
|
|
952
|
-
maxPages: Number(options.maxPages),
|
|
953
|
-
following: !options.forYou,
|
|
954
|
-
refresh: Boolean(options.refresh),
|
|
955
|
-
cacheTtlMs: Number(options.cacheTtl) * 1000,
|
|
956
|
-
});
|
|
957
|
-
await autoSyncAfterWrite();
|
|
958
|
-
print(result, true);
|
|
959
|
-
});
|
|
960
|
-
|
|
961
|
-
syncCommand
|
|
962
|
-
.command("mentions")
|
|
963
|
-
.description("Refresh live mentions through xurl or bird")
|
|
964
|
-
.option("--account <accountId>", "Account id")
|
|
965
|
-
.option("--mode <mode>", "auto, bird, or xurl", "auto")
|
|
966
|
-
.option("--limit <n>", "Result limit per page", "20")
|
|
967
|
-
.option("--max-pages <n>", "Stop after N pages")
|
|
968
|
-
.option("--since-id <id>", "Fetch mentions newer than this tweet id")
|
|
969
|
-
.option("--start-time <iso>", "Fetch mentions created at or after this time")
|
|
970
|
-
.option("--refresh", "Bypass live-cache freshness window")
|
|
971
|
-
.option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
|
|
972
|
-
.action(async (options) => {
|
|
973
|
-
try {
|
|
974
|
-
const result = await syncMentions({
|
|
975
|
-
account: options.account,
|
|
976
|
-
mode: options.mode,
|
|
977
|
-
limit: Number(options.limit),
|
|
978
|
-
maxPages: options.maxPages ? Number(options.maxPages) : undefined,
|
|
979
|
-
sinceId: options.sinceId,
|
|
980
|
-
startTime: options.startTime,
|
|
981
|
-
refresh: Boolean(options.refresh),
|
|
982
|
-
cacheTtlMs: Number(options.cacheTtl) * 1000,
|
|
983
|
-
});
|
|
984
|
-
await autoSyncAfterWrite();
|
|
985
|
-
print(result, true);
|
|
986
|
-
if (result.partial) {
|
|
987
|
-
process.exitCode = 5;
|
|
988
|
-
}
|
|
989
|
-
} catch (error) {
|
|
990
|
-
print(
|
|
991
|
-
{
|
|
992
|
-
ok: false,
|
|
993
|
-
kind: "mentions",
|
|
994
|
-
mode: options.mode ?? "xurl",
|
|
995
|
-
error: errorMessage(error),
|
|
996
|
-
},
|
|
997
|
-
true,
|
|
998
|
-
);
|
|
999
|
-
process.exitCode = 1;
|
|
1000
|
-
}
|
|
1001
|
-
});
|
|
1002
|
-
|
|
1003
|
-
syncCommand
|
|
1004
|
-
.command("authored")
|
|
1005
|
-
.description("Refresh authenticated authored tweets through xurl")
|
|
1006
|
-
.option("--account <accountId>", "Account id")
|
|
1007
|
-
.option("--mode <mode>", "xurl", "xurl")
|
|
1008
|
-
.option("--limit <n>", "X API page size", "100")
|
|
1009
|
-
.option("--max-pages <n>", "Stop after N pages and resume later")
|
|
1010
|
-
.option("--since-id <tweetId>", "Override the stored since_id cursor")
|
|
1011
|
-
.option(
|
|
1012
|
-
"--until-id <tweetId>",
|
|
1013
|
-
"Fetch tweets older than this id without moving the cursor",
|
|
1014
|
-
)
|
|
1015
|
-
.action(async (options) => {
|
|
1016
|
-
try {
|
|
1017
|
-
const result = await syncAuthoredTweets({
|
|
1018
|
-
account: options.account,
|
|
1019
|
-
mode: options.mode as AuthoredSyncMode,
|
|
1020
|
-
limit: Number(options.limit),
|
|
1021
|
-
maxPages: options.maxPages ? Number(options.maxPages) : undefined,
|
|
1022
|
-
sinceId: options.sinceId,
|
|
1023
|
-
untilId: options.untilId,
|
|
1024
|
-
});
|
|
1025
|
-
await autoSyncAfterWrite();
|
|
1026
|
-
print(result, true);
|
|
1027
|
-
if (result.partial) {
|
|
1028
|
-
process.exitCode = 5;
|
|
1029
|
-
}
|
|
1030
|
-
} catch (error) {
|
|
1031
|
-
print(
|
|
1032
|
-
{
|
|
1033
|
-
ok: false,
|
|
1034
|
-
kind: "authored",
|
|
1035
|
-
source: "xurl",
|
|
1036
|
-
error: errorMessage(error),
|
|
1037
|
-
},
|
|
1038
|
-
true,
|
|
1039
|
-
);
|
|
1040
|
-
process.exitCode =
|
|
1041
|
-
error instanceof AuthoredSyncError ? error.exitCode : 1;
|
|
1042
|
-
}
|
|
1043
|
-
});
|
|
1044
|
-
|
|
1045
|
-
syncCommand
|
|
1046
|
-
.command("mention-threads")
|
|
1047
|
-
.description(
|
|
1048
|
-
"Fetch tweet conversation context for recent mentions through bird or xurl",
|
|
1049
|
-
)
|
|
1050
|
-
.option("--account <accountId>", "Account id")
|
|
1051
|
-
.option("--mode <mode>", "bird or xurl", "bird")
|
|
1052
|
-
.option("--limit <n>", "Recent mentions to inspect", "30")
|
|
1053
|
-
.option("--delay-ms <n>", "Delay between thread fetches", "1500")
|
|
1054
|
-
.option("--timeout-ms <n>", "Per-thread timeout", "15000")
|
|
1055
|
-
.option("--all", "Fetch all retrievable thread pages")
|
|
1056
|
-
.option("--max-pages <n>", "Stop after N pages")
|
|
1057
|
-
.action(async (options) => {
|
|
1058
|
-
try {
|
|
1059
|
-
const result = await syncMentionThreads({
|
|
1060
|
-
account: options.account,
|
|
1061
|
-
mode: options.mode,
|
|
1062
|
-
limit: Number(options.limit),
|
|
1063
|
-
delayMs: Number(options.delayMs),
|
|
1064
|
-
timeoutMs: Number(options.timeoutMs),
|
|
1065
|
-
all: Boolean(options.all),
|
|
1066
|
-
maxPages: options.maxPages ? Number(options.maxPages) : undefined,
|
|
1067
|
-
});
|
|
1068
|
-
await autoSyncAfterWrite();
|
|
1069
|
-
print(result, true);
|
|
1070
|
-
if (result.partial) {
|
|
1071
|
-
process.exitCode = 5;
|
|
1072
|
-
}
|
|
1073
|
-
} catch (error) {
|
|
1074
|
-
print(
|
|
1075
|
-
{
|
|
1076
|
-
ok: false,
|
|
1077
|
-
kind: "mention-threads",
|
|
1078
|
-
mode: options.mode ?? "bird",
|
|
1079
|
-
error: errorMessage(error),
|
|
1080
|
-
},
|
|
1081
|
-
true,
|
|
1082
|
-
);
|
|
1083
|
-
process.exitCode = 1;
|
|
1084
|
-
}
|
|
1085
|
-
});
|
|
1086
|
-
|
|
1087
|
-
for (const kind of ["likes", "bookmarks"] as const) {
|
|
1088
|
-
syncCommand
|
|
1089
|
-
.command(kind)
|
|
1090
|
-
.description(`Refresh live ${kind} through xurl or bird`)
|
|
1091
|
-
.option("--account <accountId>", "Account id")
|
|
1092
|
-
.option("--mode <mode>", "auto, xurl, or bird", "auto")
|
|
1093
|
-
.option("--limit <n>", "Per-page/result limit", "20")
|
|
1094
|
-
.option("--all", "Fetch every retrievable page")
|
|
1095
|
-
.option(
|
|
1096
|
-
"--max-pages <n>",
|
|
1097
|
-
"Stop after N pages when using --all or --early-stop",
|
|
1098
|
-
)
|
|
1099
|
-
.option("--early-stop", "Stop when a fetched page is already fully local")
|
|
1100
|
-
.option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
|
|
1101
|
-
.option("--refresh", "Bypass live-cache freshness window")
|
|
1102
|
-
.action(async (options) => {
|
|
1103
|
-
const result = await syncTimelineCollection({
|
|
1104
|
-
kind,
|
|
1105
|
-
account: options.account,
|
|
1106
|
-
mode: options.mode as TimelineCollectionMode,
|
|
1107
|
-
limit: Number(options.limit),
|
|
1108
|
-
all: Boolean(options.all) || options.maxPages !== undefined,
|
|
1109
|
-
maxPages: options.maxPages ? Number(options.maxPages) : undefined,
|
|
1110
|
-
refresh: Boolean(options.refresh),
|
|
1111
|
-
cacheTtlMs: Number(options.cacheTtl) * 1000,
|
|
1112
|
-
earlyStop: Boolean(options.earlyStop),
|
|
1113
|
-
});
|
|
1114
|
-
await autoSyncAfterWrite();
|
|
1115
|
-
print(result, true);
|
|
1116
|
-
});
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
for (const direction of ["followers", "following"] as const) {
|
|
1120
|
-
syncCommand
|
|
1121
|
-
.command(direction)
|
|
1122
|
-
.description(
|
|
1123
|
-
`Dry-run or refresh live ${direction} into the local follow graph`,
|
|
1124
|
-
)
|
|
1125
|
-
.option("--account <accountId>", "Account id")
|
|
1126
|
-
.option("--mode <mode>", "auto, bird, or xurl", "auto")
|
|
1127
|
-
.option("--limit <n>", "X API users per page", "1000")
|
|
1128
|
-
.option("--max-pages <n>", "Stop after N pages")
|
|
1129
|
-
.option("--max-resources <n>", "Stop after N unique users")
|
|
1130
|
-
.option("--cache-ttl <seconds>", "Live-cache freshness window", "86400")
|
|
1131
|
-
.option("--refresh", "Bypass the live-cache freshness window")
|
|
1132
|
-
.option("--allow-partial", "Acknowledge capped/incomplete snapshot")
|
|
1133
|
-
.option("--yes", "Confirm live sync or fresh-cache merge")
|
|
1134
|
-
.action(async (options) => {
|
|
1135
|
-
try {
|
|
1136
|
-
const result = await syncFollowGraph({
|
|
1137
|
-
direction,
|
|
1138
|
-
account: options.account,
|
|
1139
|
-
mode: options.mode,
|
|
1140
|
-
limit: Number(options.limit),
|
|
1141
|
-
maxPages: options.maxPages ? Number(options.maxPages) : undefined,
|
|
1142
|
-
maxResources: options.maxResources
|
|
1143
|
-
? Number(options.maxResources)
|
|
1144
|
-
: undefined,
|
|
1145
|
-
cacheTtlMs: Number(options.cacheTtl) * 1000,
|
|
1146
|
-
refresh: Boolean(options.refresh),
|
|
1147
|
-
allowPartial: Boolean(options.allowPartial),
|
|
1148
|
-
yes: Boolean(options.yes),
|
|
1149
|
-
});
|
|
1150
|
-
if (!result.dryRun) {
|
|
1151
|
-
await autoSyncAfterWrite();
|
|
1152
|
-
}
|
|
1153
|
-
print(result, true);
|
|
1154
|
-
} catch (error) {
|
|
1155
|
-
print({ ok: false, direction, error: errorMessage(error) }, true);
|
|
1156
|
-
process.exitCode = 1;
|
|
1157
|
-
}
|
|
1158
|
-
});
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
const jobsCommand = program
|
|
1162
|
-
.command("jobs")
|
|
1163
|
-
.description("Run and install background Birdclaw jobs");
|
|
1164
|
-
|
|
1165
|
-
jobsCommand
|
|
1166
|
-
.command("sync-account")
|
|
1167
|
-
.description("Refresh live account timelines and append a JSONL audit entry")
|
|
1168
|
-
.option("--account <accountId>", "Account id")
|
|
1169
|
-
.option(
|
|
1170
|
-
"--steps <steps>",
|
|
1171
|
-
"Comma list: timeline,mentions,mention-threads,likes,bookmarks,dms",
|
|
1172
|
-
)
|
|
1173
|
-
.option("--mode <mode>", "auto, xurl, or bird for likes/bookmarks", "auto")
|
|
1174
|
-
.option("--limit <n>", "Per-page/result limit", "100")
|
|
1175
|
-
.option("--max-pages <n>", "Stop after N pages", "3")
|
|
1176
|
-
.option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
|
|
1177
|
-
.option("--refresh", "Bypass live-cache freshness window")
|
|
1178
|
-
.option(
|
|
1179
|
-
"--allow-bird-account",
|
|
1180
|
-
"Assert bird cookies match --account for Bird-backed steps",
|
|
1181
|
-
)
|
|
1182
|
-
.option("--log <path>", "Audit JSONL path")
|
|
1183
|
-
.action(async (options) => {
|
|
1184
|
-
const result = await runAccountSyncJob({
|
|
1185
|
-
account: options.account,
|
|
1186
|
-
steps: parseAccountSyncSteps(options.steps),
|
|
1187
|
-
mode: options.mode as TimelineCollectionMode,
|
|
1188
|
-
limit: Number(options.limit),
|
|
1189
|
-
maxPages: Number(options.maxPages),
|
|
1190
|
-
refresh: Boolean(options.refresh),
|
|
1191
|
-
cacheTtlMs: Number(options.cacheTtl) * 1000,
|
|
1192
|
-
allowBirdAccount: Boolean(options.allowBirdAccount),
|
|
1193
|
-
logPath: options.log,
|
|
1194
|
-
});
|
|
1195
|
-
print(result, true);
|
|
1196
|
-
if (!result.ok) {
|
|
1197
|
-
process.exitCode = 1;
|
|
1198
|
-
}
|
|
1199
|
-
});
|
|
1200
|
-
|
|
1201
|
-
jobsCommand
|
|
1202
|
-
.command("install-account-launchd")
|
|
1203
|
-
.description("Install a LaunchAgent that runs account sync")
|
|
1204
|
-
.option("--label <label>", "LaunchAgent label")
|
|
1205
|
-
.option("--interval-seconds <seconds>", "Launch interval", "1800")
|
|
1206
|
-
.option("--program <path>", "birdclaw executable or command", "birdclaw")
|
|
1207
|
-
.option("--account <accountId>", "Account id")
|
|
1208
|
-
.option(
|
|
1209
|
-
"--steps <steps>",
|
|
1210
|
-
"Comma list: timeline,mentions,mention-threads,likes,bookmarks,dms",
|
|
1211
|
-
)
|
|
1212
|
-
.option("--mode <mode>", "auto, xurl, or bird for likes/bookmarks", "auto")
|
|
1213
|
-
.option("--limit <n>", "Per-page/result limit", "100")
|
|
1214
|
-
.option("--max-pages <n>", "Stop after N pages", "3")
|
|
1215
|
-
.option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
|
|
1216
|
-
.option("--no-refresh", "Allow live-cache reuse")
|
|
1217
|
-
.option(
|
|
1218
|
-
"--allow-bird-account",
|
|
1219
|
-
"Assert bird cookies match --account for Bird-backed steps",
|
|
1220
|
-
)
|
|
1221
|
-
.option("--log <path>", "Audit JSONL path")
|
|
1222
|
-
.option("--env-path <path>", "Shell env file to source before running")
|
|
1223
|
-
.option("--env-file <path>", "Deprecated alias for --env-path")
|
|
1224
|
-
.option("--stdout <path>", "launchd stdout path")
|
|
1225
|
-
.option("--stderr <path>", "launchd stderr path")
|
|
1226
|
-
.option("--launch-agents-dir <path>", "LaunchAgents directory")
|
|
1227
|
-
.option("--no-load", "Write plist without loading it")
|
|
1228
|
-
.action(async (options) => {
|
|
1229
|
-
const result = await installAccountSyncLaunchAgent({
|
|
1230
|
-
label: options.label,
|
|
1231
|
-
intervalSeconds: Number(options.intervalSeconds),
|
|
1232
|
-
program: options.program,
|
|
1233
|
-
account: options.account,
|
|
1234
|
-
steps: parseAccountSyncSteps(options.steps),
|
|
1235
|
-
mode: options.mode as TimelineCollectionMode,
|
|
1236
|
-
limit: Number(options.limit),
|
|
1237
|
-
maxPages: Number(options.maxPages),
|
|
1238
|
-
refresh: options.refresh,
|
|
1239
|
-
allowBirdAccount: Boolean(options.allowBirdAccount),
|
|
1240
|
-
cacheTtlSeconds: Number(options.cacheTtl),
|
|
1241
|
-
logPath: options.log,
|
|
1242
|
-
envFile: options.envPath ?? options.envFile,
|
|
1243
|
-
stdoutPath: options.stdout,
|
|
1244
|
-
stderrPath: options.stderr,
|
|
1245
|
-
launchAgentsDir: options.launchAgentsDir,
|
|
1246
|
-
load: options.load,
|
|
1247
|
-
});
|
|
1248
|
-
print(result, true);
|
|
1249
|
-
});
|
|
1250
|
-
|
|
1251
|
-
jobsCommand
|
|
1252
|
-
.command("sync-bookmarks")
|
|
1253
|
-
.description("Refresh live bookmarks and append a JSONL audit entry")
|
|
1254
|
-
.option("--account <accountId>", "Account id")
|
|
1255
|
-
.option("--mode <mode>", "auto, xurl, or bird", "auto")
|
|
1256
|
-
.option("--limit <n>", "Per-page/result limit", "100")
|
|
1257
|
-
.option("--all", "Fetch every retrievable page")
|
|
1258
|
-
.option("--max-pages <n>", "Stop after N pages", "5")
|
|
1259
|
-
.option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
|
|
1260
|
-
.option("--refresh", "Bypass live-cache freshness window")
|
|
1261
|
-
.option("--log <path>", "Audit JSONL path")
|
|
1262
|
-
.action(async (options) => {
|
|
1263
|
-
const result = await runBookmarkSyncJob({
|
|
1264
|
-
account: options.account,
|
|
1265
|
-
mode: options.mode as TimelineCollectionMode,
|
|
1266
|
-
limit: Number(options.limit),
|
|
1267
|
-
all: Boolean(options.all) || options.maxPages !== undefined,
|
|
1268
|
-
maxPages: options.all ? undefined : Number(options.maxPages),
|
|
1269
|
-
refresh: Boolean(options.refresh),
|
|
1270
|
-
cacheTtlMs: Number(options.cacheTtl) * 1000,
|
|
1271
|
-
logPath: options.log,
|
|
1272
|
-
});
|
|
1273
|
-
print(result, true);
|
|
1274
|
-
if (!result.ok) {
|
|
1275
|
-
process.exitCode = 1;
|
|
1276
|
-
}
|
|
1277
|
-
});
|
|
1278
|
-
|
|
1279
|
-
jobsCommand
|
|
1280
|
-
.command("install-bookmarks-launchd")
|
|
1281
|
-
.description("Install a LaunchAgent that runs bookmark sync every 3 hours")
|
|
1282
|
-
.option("--label <label>", "LaunchAgent label")
|
|
1283
|
-
.option("--interval-seconds <seconds>", "Launch interval", "10800")
|
|
1284
|
-
.option("--program <path>", "birdclaw executable or command", "birdclaw")
|
|
1285
|
-
.option("--mode <mode>", "auto, xurl, or bird", "auto")
|
|
1286
|
-
.option("--limit <n>", "Per-page/result limit", "100")
|
|
1287
|
-
.option("--all", "Fetch every retrievable page")
|
|
1288
|
-
.option("--max-pages <n>", "Stop after N pages", "5")
|
|
1289
|
-
.option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
|
|
1290
|
-
.option("--no-refresh", "Allow live-cache reuse")
|
|
1291
|
-
.option("--log <path>", "Audit JSONL path")
|
|
1292
|
-
.option("--env-path <path>", "Shell env file to source before running")
|
|
1293
|
-
.option("--env-file <path>", "Deprecated alias for --env-path")
|
|
1294
|
-
.option("--stdout <path>", "launchd stdout path")
|
|
1295
|
-
.option("--stderr <path>", "launchd stderr path")
|
|
1296
|
-
.option("--launch-agents-dir <path>", "LaunchAgents directory")
|
|
1297
|
-
.option("--no-load", "Write plist without loading it")
|
|
1298
|
-
.action(async (options) => {
|
|
1299
|
-
const result = await installBookmarkSyncLaunchAgent({
|
|
1300
|
-
label: options.label,
|
|
1301
|
-
intervalSeconds: Number(options.intervalSeconds),
|
|
1302
|
-
program: options.program,
|
|
1303
|
-
mode: options.mode as TimelineCollectionMode,
|
|
1304
|
-
limit: Number(options.limit),
|
|
1305
|
-
all: Boolean(options.all) || options.maxPages !== undefined,
|
|
1306
|
-
maxPages: options.all ? undefined : Number(options.maxPages),
|
|
1307
|
-
refresh: options.refresh,
|
|
1308
|
-
cacheTtlSeconds: Number(options.cacheTtl),
|
|
1309
|
-
logPath: options.log,
|
|
1310
|
-
envFile: options.envPath ?? options.envFile,
|
|
1311
|
-
stdoutPath: options.stdout,
|
|
1312
|
-
stderrPath: options.stderr,
|
|
1313
|
-
launchAgentsDir: options.launchAgentsDir,
|
|
1314
|
-
load: options.load,
|
|
1315
|
-
});
|
|
1316
|
-
print(result, true);
|
|
1317
|
-
});
|
|
1318
|
-
|
|
1319
|
-
dmsCommand
|
|
1320
|
-
.command("list")
|
|
1321
|
-
.option("--account <accountId>", "Account id")
|
|
1322
|
-
.option("--mode <mode>", "auto, bird, or xurl", "bird")
|
|
1323
|
-
.option("--refresh", "Refresh live DMs before listing")
|
|
1324
|
-
.option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
|
|
1325
|
-
.option("--inbox <kind>", "all, accepted, or requests", "all")
|
|
1326
|
-
.option("--max-pages <n>", "Additional accepted/request pages to sync", "0")
|
|
1327
|
-
.option("--all-pages", "Fetch all accepted/request pages while syncing")
|
|
1328
|
-
.option("--page-delay-ms <n>", "Delay between live DM page requests", "0")
|
|
1329
|
-
.option("--participant <value>")
|
|
1330
|
-
.option("--min-followers <n>", "Minimum sender follower count")
|
|
1331
|
-
.option("--max-followers <n>", "Maximum sender follower count")
|
|
1332
|
-
.option("--min-influence-score <n>", "Minimum derived influence score")
|
|
1333
|
-
.option("--max-influence-score <n>", "Maximum derived influence score")
|
|
1334
|
-
.option("--sort <mode>", "recent or followers", "recent")
|
|
1335
|
-
.option(
|
|
1336
|
-
"--resolve-profiles",
|
|
1337
|
-
"Resolve placeholder DM profiles through cache/bird/xurl",
|
|
1338
|
-
)
|
|
1339
|
-
.option("--expand-urls", "Expand URLs through the persistent URL cache")
|
|
1340
|
-
.option("--refresh-profile-cache", "Bypass profile lookup cache")
|
|
1341
|
-
.option("--refresh-url-cache", "Bypass URL expansion cache")
|
|
1342
|
-
.option(
|
|
1343
|
-
"--no-xurl-fallback",
|
|
1344
|
-
"Do not fall back to xurl after bird profile lookup",
|
|
1345
|
-
)
|
|
1346
|
-
.option("--replied", "Only replied threads")
|
|
1347
|
-
.option("--unreplied", "Only unreplied threads")
|
|
1348
|
-
.option("--limit <n>", "Limit results", "20")
|
|
1349
|
-
.action(async (options) => {
|
|
1350
|
-
const replyFilter = options.replied
|
|
1351
|
-
? "replied"
|
|
1352
|
-
: options.unreplied
|
|
1353
|
-
? "unreplied"
|
|
1354
|
-
: "all";
|
|
1355
|
-
const inbox = parseDmInboxOption(options.inbox);
|
|
1356
|
-
const mode = parseDmSyncModeOption(options.mode);
|
|
1357
|
-
const maxPages = parseNonNegativeIntegerOption(
|
|
1358
|
-
options.maxPages,
|
|
1359
|
-
"--max-pages",
|
|
1360
|
-
);
|
|
1361
|
-
const pageDelayMs = parseNonNegativeIntegerOption(
|
|
1362
|
-
options.pageDelayMs,
|
|
1363
|
-
"--page-delay-ms",
|
|
1364
|
-
);
|
|
1365
|
-
if (
|
|
1366
|
-
inbox === undefined ||
|
|
1367
|
-
mode === undefined ||
|
|
1368
|
-
maxPages === undefined ||
|
|
1369
|
-
pageDelayMs === undefined
|
|
1370
|
-
) {
|
|
1371
|
-
return;
|
|
1372
|
-
}
|
|
1373
|
-
if (options.refresh) {
|
|
1374
|
-
await syncDirectMessagesViaCachedBird({
|
|
1375
|
-
account: options.account,
|
|
1376
|
-
mode,
|
|
1377
|
-
limit: Number(options.limit),
|
|
1378
|
-
...(inbox !== "all" ? { inbox } : {}),
|
|
1379
|
-
...(maxPages > 0 ? { maxPages } : {}),
|
|
1380
|
-
...(options.allPages ? { allPages: true } : {}),
|
|
1381
|
-
...(pageDelayMs > 0 ? { pageDelayMs } : {}),
|
|
1382
|
-
refresh: true,
|
|
1383
|
-
cacheTtlMs: Number(options.cacheTtl) * 1000,
|
|
1384
|
-
});
|
|
1385
|
-
await autoSyncAfterWrite();
|
|
1386
|
-
} else {
|
|
1387
|
-
await autoUpdateBeforeRead();
|
|
1388
|
-
}
|
|
1389
|
-
const items = await enrichDmItems(
|
|
1390
|
-
{
|
|
1391
|
-
account: options.account,
|
|
1392
|
-
...(inbox !== "all" ? { inbox } : {}),
|
|
1393
|
-
participant: options.participant,
|
|
1394
|
-
minFollowers: options.minFollowers
|
|
1395
|
-
? Number(options.minFollowers)
|
|
1396
|
-
: undefined,
|
|
1397
|
-
maxFollowers: options.maxFollowers
|
|
1398
|
-
? Number(options.maxFollowers)
|
|
1399
|
-
: undefined,
|
|
1400
|
-
minInfluenceScore: options.minInfluenceScore
|
|
1401
|
-
? Number(options.minInfluenceScore)
|
|
1402
|
-
: undefined,
|
|
1403
|
-
maxInfluenceScore: options.maxInfluenceScore
|
|
1404
|
-
? Number(options.maxInfluenceScore)
|
|
1405
|
-
: undefined,
|
|
1406
|
-
sort:
|
|
1407
|
-
options.sort === "followers" || options.sort === "influence"
|
|
1408
|
-
? "followers"
|
|
1409
|
-
: "recent",
|
|
1410
|
-
replyFilter,
|
|
1411
|
-
limit: Number(options.limit),
|
|
1412
|
-
},
|
|
1413
|
-
{
|
|
1414
|
-
resolveProfiles: Boolean(options.resolveProfiles),
|
|
1415
|
-
expandUrls: Boolean(options.expandUrls),
|
|
1416
|
-
refreshProfileCache: Boolean(options.refreshProfileCache),
|
|
1417
|
-
refreshUrlCache: Boolean(options.refreshUrlCache),
|
|
1418
|
-
xurlFallback: options.xurlFallback,
|
|
1419
|
-
},
|
|
1420
|
-
);
|
|
1421
|
-
print(items, program.opts().json ?? false);
|
|
1422
|
-
});
|
|
1423
|
-
|
|
1424
|
-
dmsCommand
|
|
1425
|
-
.command("sync")
|
|
1426
|
-
.description("Refresh live direct messages into the local store")
|
|
1427
|
-
.option("--account <accountId>", "Account id")
|
|
1428
|
-
.option("--mode <mode>", "auto, bird, or xurl", "bird")
|
|
1429
|
-
.option("--limit <n>", "Limit messages", "20")
|
|
1430
|
-
.option("--inbox <kind>", "all, accepted, or requests", "all")
|
|
1431
|
-
.option("--max-pages <n>", "Additional accepted/request pages to sync", "0")
|
|
1432
|
-
.option("--all-pages", "Fetch all accepted/request pages")
|
|
1433
|
-
.option("--page-delay-ms <n>", "Delay between live DM page requests", "0")
|
|
1434
|
-
.option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
|
|
1435
|
-
.option("--refresh", "Bypass live-cache freshness window")
|
|
1436
|
-
.action(async (options) => {
|
|
1437
|
-
const inbox = parseDmInboxOption(options.inbox);
|
|
1438
|
-
const mode = parseDmSyncModeOption(options.mode);
|
|
1439
|
-
const maxPages = parseNonNegativeIntegerOption(
|
|
1440
|
-
options.maxPages,
|
|
1441
|
-
"--max-pages",
|
|
1442
|
-
);
|
|
1443
|
-
const pageDelayMs = parseNonNegativeIntegerOption(
|
|
1444
|
-
options.pageDelayMs,
|
|
1445
|
-
"--page-delay-ms",
|
|
1446
|
-
);
|
|
1447
|
-
if (
|
|
1448
|
-
inbox === undefined ||
|
|
1449
|
-
mode === undefined ||
|
|
1450
|
-
maxPages === undefined ||
|
|
1451
|
-
pageDelayMs === undefined
|
|
1452
|
-
) {
|
|
1453
|
-
return;
|
|
1454
|
-
}
|
|
1455
|
-
const result = await syncDirectMessagesViaCachedBird({
|
|
1456
|
-
account: options.account,
|
|
1457
|
-
mode,
|
|
1458
|
-
limit: Number(options.limit),
|
|
1459
|
-
...(inbox !== "all" ? { inbox } : {}),
|
|
1460
|
-
...(maxPages > 0 ? { maxPages } : {}),
|
|
1461
|
-
...(options.allPages ? { allPages: true } : {}),
|
|
1462
|
-
...(pageDelayMs > 0 ? { pageDelayMs } : {}),
|
|
1463
|
-
refresh: Boolean(options.refresh),
|
|
1464
|
-
cacheTtlMs: Number(options.cacheTtl) * 1000,
|
|
1465
|
-
});
|
|
1466
|
-
await autoSyncAfterWrite();
|
|
1467
|
-
print(result, true);
|
|
1468
|
-
});
|
|
1469
|
-
|
|
1470
|
-
for (const action of ["accept", "reject", "block"] as const) {
|
|
1471
|
-
const command = dmsCommand
|
|
1472
|
-
.command(`${action} <conversationId>`)
|
|
1473
|
-
.description(`${action} a live DM message request through bird`);
|
|
1474
|
-
if (action === "block") {
|
|
1475
|
-
command
|
|
1476
|
-
.option("--max-pages <n>", "Additional timeline pages to search", "3")
|
|
1477
|
-
.option("--all-pages", "Search all accepted/request timeline pages");
|
|
1478
|
-
}
|
|
1479
|
-
command.action(async (conversationId, options) => {
|
|
1480
|
-
const maxPages =
|
|
1481
|
-
action === "block"
|
|
1482
|
-
? parseNonNegativeIntegerOption(options.maxPages, "--max-pages")
|
|
1483
|
-
: undefined;
|
|
1484
|
-
if (action === "block" && maxPages === undefined) {
|
|
1485
|
-
return;
|
|
1486
|
-
}
|
|
1487
|
-
const result = await runDirectMessageRequestMutationViaBird({
|
|
1488
|
-
action,
|
|
1489
|
-
conversationId,
|
|
1490
|
-
...(action === "block" && maxPages !== undefined ? { maxPages } : {}),
|
|
1491
|
-
...(action === "block" && options.allPages ? { allPages: true } : {}),
|
|
1492
|
-
});
|
|
1493
|
-
if (result.success) {
|
|
1494
|
-
await applyDmRequestMutationToLocalStore(conversationId, action);
|
|
1495
|
-
} else {
|
|
1496
|
-
process.exitCode = 1;
|
|
1497
|
-
}
|
|
1498
|
-
await autoSyncAfterWrite();
|
|
1499
|
-
print(result, true);
|
|
1500
|
-
});
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
registerModerationCommands({
|
|
1504
|
-
program,
|
|
1505
|
-
print,
|
|
1506
|
-
asJson: () => program.opts().json ?? false,
|
|
1507
|
-
importBlocklist,
|
|
1508
|
-
resolveActionOptions,
|
|
1509
|
-
});
|
|
1510
|
-
|
|
1511
|
-
const commandContext = {
|
|
1512
|
-
program,
|
|
1513
|
-
print,
|
|
1514
|
-
asJson: () => program.opts().json ?? false,
|
|
1515
|
-
autoSyncAfterWrite,
|
|
1516
|
-
autoUpdateBeforeRead,
|
|
1517
|
-
parseNonNegativeIntegerOption,
|
|
1518
|
-
parsePositiveIntegerOption,
|
|
1519
|
-
};
|
|
1520
|
-
|
|
1521
|
-
registerComposeCommands(commandContext);
|
|
1522
|
-
registerInboxCommand(commandContext);
|
|
1523
|
-
|
|
1524
|
-
registerGraphCommands(commandContext);
|
|
1525
|
-
|
|
1526
|
-
registerStorageCommands(commandContext);
|
|
1527
|
-
|
|
1528
|
-
program
|
|
1529
|
-
.command("serve")
|
|
1530
|
-
.description("Run the local web app")
|
|
1531
|
-
.action(async () => {
|
|
1532
|
-
await autoUpdateBeforeRead();
|
|
1533
|
-
const child = spawn(
|
|
1534
|
-
process.execPath,
|
|
1535
|
-
[
|
|
1536
|
-
"node_modules/vite/bin/vite.js",
|
|
1537
|
-
"dev",
|
|
1538
|
-
"--host",
|
|
1539
|
-
"127.0.0.1",
|
|
1540
|
-
"--port",
|
|
1541
|
-
"3000",
|
|
1542
|
-
],
|
|
1543
|
-
{
|
|
1544
|
-
cwd: packageRoot,
|
|
1545
|
-
env: { ...process.env, BIRDCLAW_LOCAL_WEB: "1" },
|
|
1546
|
-
stdio: "inherit",
|
|
1547
|
-
detached: process.platform !== "win32",
|
|
1548
|
-
},
|
|
1549
|
-
);
|
|
1550
|
-
const forwardedSignals = [
|
|
1551
|
-
"SIGINT",
|
|
1552
|
-
"SIGTERM",
|
|
1553
|
-
"SIGHUP",
|
|
1554
|
-
"SIGQUIT",
|
|
1555
|
-
] as const;
|
|
1556
|
-
const forwardSignal = (signal: NodeJS.Signals) => {
|
|
1557
|
-
if (child.exitCode === null && child.signalCode === null) {
|
|
1558
|
-
signalChild(signal);
|
|
1559
|
-
}
|
|
1560
|
-
};
|
|
1561
|
-
const signalChild = (signal: NodeJS.Signals) => {
|
|
1562
|
-
if (child.pid === undefined) {
|
|
1563
|
-
return;
|
|
1564
|
-
}
|
|
1565
|
-
const targetPid = process.platform === "win32" ? child.pid : -child.pid;
|
|
1566
|
-
try {
|
|
1567
|
-
process.kill(targetPid, signal);
|
|
1568
|
-
} catch (error) {
|
|
1569
|
-
if (
|
|
1570
|
-
!(
|
|
1571
|
-
typeof error === "object" &&
|
|
1572
|
-
error !== null &&
|
|
1573
|
-
"code" in error &&
|
|
1574
|
-
error.code === "ESRCH"
|
|
1575
|
-
)
|
|
1576
|
-
) {
|
|
1577
|
-
throw error;
|
|
1578
|
-
}
|
|
1579
|
-
}
|
|
1580
|
-
};
|
|
1581
|
-
const removeSignalHandlers = () => {
|
|
1582
|
-
for (const signal of forwardedSignals) {
|
|
1583
|
-
process.removeListener(signal, forwardSignal);
|
|
1584
|
-
}
|
|
1585
|
-
};
|
|
1586
|
-
for (const signal of forwardedSignals) {
|
|
1587
|
-
process.on(signal, forwardSignal);
|
|
1588
|
-
}
|
|
1589
|
-
child.on("exit", (code, signal) => {
|
|
1590
|
-
removeSignalHandlers();
|
|
1591
|
-
if (signal) {
|
|
1592
|
-
process.kill(process.pid, signal);
|
|
1593
|
-
return;
|
|
1594
|
-
}
|
|
1595
|
-
process.exit(code ?? 0);
|
|
1596
|
-
});
|
|
1597
|
-
});
|
|
1598
|
-
|
|
1599
|
-
export async function runCli(argv = process.argv) {
|
|
1600
|
-
try {
|
|
1601
|
-
await program.parseAsync(argv);
|
|
1602
|
-
} finally {
|
|
1603
|
-
await closeDatabase();
|
|
1604
|
-
}
|
|
1605
|
-
}
|
|
1606
|
-
|
|
1607
|
-
/* v8 ignore next 5 */
|
|
1608
|
-
if (process.argv[1]) {
|
|
1609
|
-
const entryUrl = pathToFileURL(process.argv[1]).href;
|
|
1610
|
-
if (import.meta.url === entryUrl) {
|
|
1611
|
-
void runCli().catch((error) => {
|
|
1612
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
1613
|
-
process.exitCode = 1;
|
|
1614
|
-
});
|
|
1615
|
-
}
|
|
1616
|
-
}
|