birdclaw 0.8.2 → 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 +33 -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 +27 -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-moderation.ts +0 -210
- package/src/cli.ts +0 -2336
- package/src/components/AccountSwitcher.tsx +0 -186
- package/src/components/AppNav.tsx +0 -118
- package/src/components/AvatarChip.tsx +0 -58
- 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/MarkdownViewer.tsx +0 -777
- package/src/components/ProfileAnalysisStream.tsx +0 -428
- package/src/components/ProfilePreview.tsx +0 -129
- 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/useTimelineRouteData.ts +0 -323
- package/src/lib/account-sync-job.ts +0 -666
- package/src/lib/actions-transport.ts +0 -300
- package/src/lib/api-client.ts +0 -419
- package/src/lib/archive-finder.ts +0 -185
- package/src/lib/archive-import.ts +0 -2764
- package/src/lib/authored-live.ts +0 -1072
- package/src/lib/avatar-cache.ts +0 -349
- package/src/lib/backup.ts +0 -2741
- 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 -167
- package/src/lib/blocks.ts +0 -389
- package/src/lib/bookmark-sync-job.ts +0 -536
- package/src/lib/client-cache.ts +0 -109
- 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/db.ts +0 -1135
- package/src/lib/dms-live.ts +0 -933
- package/src/lib/effect-runtime.ts +0 -45
- package/src/lib/follow-graph.ts +0 -1097
- 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/inbox.ts +0 -241
- 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/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 -917
- package/src/lib/mentions-export.ts +0 -147
- package/src/lib/mentions-live.ts +0 -1238
- package/src/lib/moderation-target.ts +0 -216
- package/src/lib/moderation-write.ts +0 -131
- package/src/lib/mutes-write.ts +0 -167
- 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.ts +0 -117
- package/src/lib/period-digest.ts +0 -1552
- 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 -1272
- 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 -2381
- package/src/lib/research.ts +0 -652
- package/src/lib/search-discussion.ts +0 -1016
- package/src/lib/seed.ts +0 -613
- package/src/lib/sqlite.ts +0 -147
- package/src/lib/sync-cache.ts +0 -65
- package/src/lib/theme-transition.ts +0 -113
- package/src/lib/theme.tsx +0 -165
- package/src/lib/timeline-collections-live.ts +0 -581
- package/src/lib/timeline-live.ts +0 -452
- 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-search-live.ts +0 -566
- 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 -516
- package/src/lib/whois.ts +0 -1079
- 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 -79
- 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 -115
- package/src/routes/api/search-discussion.tsx +0 -113
- package/src/routes/api/status.tsx +0 -28
- package/src/routes/api/sync.tsx +0 -105
- package/src/routes/api/xurl-rate-limits.tsx +0 -24
- package/src/routes/blocks.tsx +0 -377
- package/src/routes/bookmarks.tsx +0 -18
- package/src/routes/data-sources.tsx +0 -257
- package/src/routes/discuss.tsx +0 -419
- package/src/routes/dms.tsx +0 -432
- package/src/routes/inbox.tsx +0 -226
- package/src/routes/index.tsx +0 -32
- package/src/routes/likes.tsx +0 -18
- package/src/routes/links.tsx +0 -994
- package/src/routes/mentions.tsx +0 -32
- package/src/routes/network-map.tsx +0 -1035
- package/src/routes/profile-analyze.tsx +0 -112
- package/src/routes/profiles.$handle.tsx +0 -228
- package/src/routes/rate-limits.tsx +0 -309
- 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,2336 +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 { findArchives } from "#/lib/archive-finder";
|
|
10
|
-
import {
|
|
11
|
-
ARCHIVE_IMPORT_SLICES,
|
|
12
|
-
type ArchiveImportSlice,
|
|
13
|
-
type ImportProgressEvent,
|
|
14
|
-
type ImportProgressSlice,
|
|
15
|
-
type ImportWritePhase,
|
|
16
|
-
importArchive,
|
|
17
|
-
} from "#/lib/archive-import";
|
|
18
|
-
import {
|
|
19
|
-
AuthoredSyncError,
|
|
20
|
-
syncAuthoredTweets,
|
|
21
|
-
type AuthoredSyncMode,
|
|
22
|
-
} from "#/lib/authored-live";
|
|
23
|
-
import {
|
|
24
|
-
installAccountSyncLaunchAgent,
|
|
25
|
-
parseAccountSyncSteps,
|
|
26
|
-
runAccountSyncJob,
|
|
27
|
-
} from "#/lib/account-sync-job";
|
|
28
|
-
import {
|
|
29
|
-
exportBackup,
|
|
30
|
-
importBackup,
|
|
31
|
-
maybeAutoSyncBackup,
|
|
32
|
-
maybeAutoUpdateBackup,
|
|
33
|
-
syncBackup,
|
|
34
|
-
validateBackup,
|
|
35
|
-
} from "#/lib/backup";
|
|
36
|
-
import {
|
|
37
|
-
installBookmarkSyncLaunchAgent,
|
|
38
|
-
runBookmarkSyncJob,
|
|
39
|
-
} from "#/lib/bookmark-sync-job";
|
|
40
|
-
import { runDirectMessageRequestMutationViaBird } from "#/lib/bird";
|
|
41
|
-
import { importBlocklist } from "#/lib/blocklist";
|
|
42
|
-
import {
|
|
43
|
-
type ActionsTransport,
|
|
44
|
-
ensureBirdclawDirs,
|
|
45
|
-
getBirdclawPaths,
|
|
46
|
-
resolveMentionsDataSource,
|
|
47
|
-
setActionsTransport,
|
|
48
|
-
} from "#/lib/config";
|
|
49
|
-
import { closeDatabase } from "#/lib/db";
|
|
50
|
-
import {
|
|
51
|
-
type DirectMessagesSyncMode,
|
|
52
|
-
syncDirectMessagesViaCachedBird,
|
|
53
|
-
} from "#/lib/dms-live";
|
|
54
|
-
import { listInboxItems, scoreInbox } from "#/lib/inbox";
|
|
55
|
-
import { backfillLinkIndex, searchLinks } from "#/lib/link-index";
|
|
56
|
-
import { fetchTweetMedia, formatMediaFetchResult } from "#/lib/media-fetch";
|
|
57
|
-
import { syncMentionThreads } from "#/lib/mention-threads-live";
|
|
58
|
-
import { exportMentionItems } from "#/lib/mentions-export";
|
|
59
|
-
import {
|
|
60
|
-
exportMentionsViaCachedAuto,
|
|
61
|
-
exportMentionsViaCachedBird,
|
|
62
|
-
exportMentionsViaCachedXurl,
|
|
63
|
-
syncMentions,
|
|
64
|
-
} from "#/lib/mentions-live";
|
|
65
|
-
import {
|
|
66
|
-
normalizeDigestLanguage,
|
|
67
|
-
streamPeriodDigest,
|
|
68
|
-
type PeriodDigestOptions,
|
|
69
|
-
type PeriodDigestPreset,
|
|
70
|
-
} from "#/lib/period-digest";
|
|
71
|
-
import {
|
|
72
|
-
streamProfileAnalysis,
|
|
73
|
-
type ProfileAnalysisOptions,
|
|
74
|
-
} from "#/lib/profile-analysis";
|
|
75
|
-
import {
|
|
76
|
-
getFollowGraphSummary,
|
|
77
|
-
listFollowEvents,
|
|
78
|
-
listMutuals,
|
|
79
|
-
listNonMutualFollowing,
|
|
80
|
-
listTopFollowers,
|
|
81
|
-
listUnfollowedSince,
|
|
82
|
-
syncFollowGraph,
|
|
83
|
-
} from "#/lib/follow-graph";
|
|
84
|
-
import { hydrateProfilesFromX } from "#/lib/profile-hydration";
|
|
85
|
-
import { resolveProfilesForIds } from "#/lib/profile-resolver";
|
|
86
|
-
import { inspectProfileReplies } from "#/lib/profile-replies";
|
|
87
|
-
import { runResearchMode } from "#/lib/research";
|
|
88
|
-
import {
|
|
89
|
-
streamSearchDiscussion,
|
|
90
|
-
type SearchDiscussionOptions,
|
|
91
|
-
type SearchDiscussionSource,
|
|
92
|
-
} from "#/lib/search-discussion";
|
|
93
|
-
import {
|
|
94
|
-
applyDmRequestMutationToLocalStore,
|
|
95
|
-
createDmReply,
|
|
96
|
-
createPost,
|
|
97
|
-
createTweetReply,
|
|
98
|
-
getQueryEnvelope,
|
|
99
|
-
listDmConversations,
|
|
100
|
-
listTimelineItems,
|
|
101
|
-
} from "#/lib/queries";
|
|
102
|
-
import {
|
|
103
|
-
syncTimelineCollection,
|
|
104
|
-
type TimelineCollectionMode,
|
|
105
|
-
} from "#/lib/timeline-collections-live";
|
|
106
|
-
import { syncHomeTimeline } from "#/lib/timeline-live";
|
|
107
|
-
import { expandUrlsFromTexts } from "#/lib/url-expansion";
|
|
108
|
-
import { formatWhois, runWhois } from "#/lib/whois";
|
|
109
|
-
|
|
110
|
-
const program = new Command();
|
|
111
|
-
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
112
|
-
const packageVersion = JSON.parse(
|
|
113
|
-
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
114
|
-
) as { version?: string };
|
|
115
|
-
|
|
116
|
-
function print(data: unknown, asJson: boolean) {
|
|
117
|
-
if (asJson) {
|
|
118
|
-
console.log(JSON.stringify(data, null, 2));
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
console.log(data);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function printError(error: string) {
|
|
125
|
-
console.error(JSON.stringify({ error }));
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function errorMessage(error: unknown) {
|
|
129
|
-
return error instanceof Error ? error.message : String(error);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const IMPORT_SLICE_LABELS: Record<ImportProgressSlice, string> = {
|
|
133
|
-
tweets: "tweets",
|
|
134
|
-
noteTweets: "note tweets",
|
|
135
|
-
directMessages: "direct messages",
|
|
136
|
-
likes: "likes",
|
|
137
|
-
bookmarks: "bookmarks",
|
|
138
|
-
media: "media files",
|
|
139
|
-
followers: "followers",
|
|
140
|
-
following: "following",
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
const IMPORT_WRITE_LABELS: Record<ImportWritePhase, string> = {
|
|
144
|
-
profiles: "profiles",
|
|
145
|
-
tweets: "tweets",
|
|
146
|
-
collections: "likes+bookmarks",
|
|
147
|
-
dmMessages: "DM messages",
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
function logImportProgress(event: ImportProgressEvent) {
|
|
151
|
-
switch (event.kind) {
|
|
152
|
-
case "scanned":
|
|
153
|
-
process.stderr.write(
|
|
154
|
-
`Scanning archive… ${String(event.entryCount)} entries\n`,
|
|
155
|
-
);
|
|
156
|
-
return;
|
|
157
|
-
case "slice-start":
|
|
158
|
-
if (event.slice === "media") {
|
|
159
|
-
process.stderr.write("Indexing media files…\n");
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
process.stderr.write(
|
|
163
|
-
`Parsing ${IMPORT_SLICE_LABELS[event.slice]}… (${String(event.files)} file${event.files === 1 ? "" : "s"})\n`,
|
|
164
|
-
);
|
|
165
|
-
return;
|
|
166
|
-
case "slice-file":
|
|
167
|
-
if (event.files > 1) {
|
|
168
|
-
process.stderr.write(
|
|
169
|
-
` ${IMPORT_SLICE_LABELS[event.slice]} ${String(event.processed)}/${String(event.files)}\n`,
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
return;
|
|
173
|
-
case "slice-done":
|
|
174
|
-
process.stderr.write(
|
|
175
|
-
` ${IMPORT_SLICE_LABELS[event.slice]}: ${event.count.toLocaleString()}\n`,
|
|
176
|
-
);
|
|
177
|
-
return;
|
|
178
|
-
case "writing":
|
|
179
|
-
process.stderr.write("Writing to database…\n");
|
|
180
|
-
return;
|
|
181
|
-
case "write-start":
|
|
182
|
-
process.stderr.write(
|
|
183
|
-
`Writing ${IMPORT_WRITE_LABELS[event.phase]}… (${event.total.toLocaleString()})\n`,
|
|
184
|
-
);
|
|
185
|
-
return;
|
|
186
|
-
case "write-progress":
|
|
187
|
-
process.stderr.write(
|
|
188
|
-
` ${IMPORT_WRITE_LABELS[event.phase]} ${event.processed.toLocaleString()}/${event.total.toLocaleString()}\n`,
|
|
189
|
-
);
|
|
190
|
-
return;
|
|
191
|
-
case "done":
|
|
192
|
-
process.stderr.write("Import complete.\n");
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function formatLinkSearchItems(items: ReturnType<typeof searchLinks>) {
|
|
198
|
-
return items
|
|
199
|
-
.map((item) => {
|
|
200
|
-
const linked = item.linkedTweet
|
|
201
|
-
? ` -> @${item.linkedTweet.author.handle}/${item.linkedTweet.id}: ${item.linkedTweet.text}`
|
|
202
|
-
: ` -> ${item.expansion.finalUrl}`;
|
|
203
|
-
const source =
|
|
204
|
-
item.occurrence.sourceKind === "dm"
|
|
205
|
-
? `dm ${item.occurrence.direction ?? ""}`.trim()
|
|
206
|
-
: "tweet";
|
|
207
|
-
const participant = item.participant
|
|
208
|
-
? ` @${item.participant.handle}`
|
|
209
|
-
: "";
|
|
210
|
-
return `${item.occurrence.createdAt} ${source}${participant}: ${item.occurrence.shortUrl}${linked}`;
|
|
211
|
-
})
|
|
212
|
-
.join("\n");
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function parseNonNegativeIntegerOption(
|
|
216
|
-
value: string | undefined,
|
|
217
|
-
option: string,
|
|
218
|
-
) {
|
|
219
|
-
if (value === undefined) {
|
|
220
|
-
return undefined;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const trimmed = value.trim();
|
|
224
|
-
if (!/^\d+$/.test(trimmed)) {
|
|
225
|
-
printError(`${option} must be a non-negative integer`);
|
|
226
|
-
process.exitCode = 1;
|
|
227
|
-
return undefined;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const parsed = Number.parseInt(trimmed, 10);
|
|
231
|
-
if (!Number.isSafeInteger(parsed)) {
|
|
232
|
-
printError(`${option} must be a non-negative integer`);
|
|
233
|
-
process.exitCode = 1;
|
|
234
|
-
return undefined;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return parsed;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function parsePositiveIntegerOption(value: string | undefined, option: string) {
|
|
241
|
-
const parsed = parseNonNegativeIntegerOption(value, option);
|
|
242
|
-
if (parsed === undefined) {
|
|
243
|
-
return undefined;
|
|
244
|
-
}
|
|
245
|
-
if (parsed < 1) {
|
|
246
|
-
printError(`${option} must be at least 1`);
|
|
247
|
-
process.exitCode = 1;
|
|
248
|
-
return undefined;
|
|
249
|
-
}
|
|
250
|
-
return parsed;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function parseDmInboxOption(
|
|
254
|
-
value: string | undefined,
|
|
255
|
-
): "all" | "accepted" | "requests" | undefined {
|
|
256
|
-
const normalized = (value ?? "all").trim().toLowerCase();
|
|
257
|
-
if (
|
|
258
|
-
normalized === "all" ||
|
|
259
|
-
normalized === "accepted" ||
|
|
260
|
-
normalized === "requests"
|
|
261
|
-
) {
|
|
262
|
-
return normalized;
|
|
263
|
-
}
|
|
264
|
-
if (normalized === "request") {
|
|
265
|
-
return "requests";
|
|
266
|
-
}
|
|
267
|
-
printError("--inbox must be all, accepted, or requests");
|
|
268
|
-
process.exitCode = 1;
|
|
269
|
-
return undefined;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function parseDmSyncModeOption(
|
|
273
|
-
value: string | undefined,
|
|
274
|
-
): DirectMessagesSyncMode | undefined {
|
|
275
|
-
const normalized = (value ?? "bird").trim().toLowerCase();
|
|
276
|
-
if (normalized === "auto" || normalized === "bird" || normalized === "xurl") {
|
|
277
|
-
return normalized;
|
|
278
|
-
}
|
|
279
|
-
printError("--mode must be auto, bird, or xurl");
|
|
280
|
-
process.exitCode = 1;
|
|
281
|
-
return undefined;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function parseDigestLiveModeOption(
|
|
285
|
-
value: string | undefined,
|
|
286
|
-
): PeriodDigestOptions["liveSyncMode"] {
|
|
287
|
-
const normalized = (value ?? "xurl").trim().toLowerCase();
|
|
288
|
-
if (normalized === "auto" || normalized === "bird" || normalized === "xurl") {
|
|
289
|
-
return normalized;
|
|
290
|
-
}
|
|
291
|
-
printError("--live-mode must be auto, bird, or xurl");
|
|
292
|
-
process.exitCode = 1;
|
|
293
|
-
return undefined;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function parseArchiveImportSelect(value: string | undefined) {
|
|
297
|
-
if (value === undefined) {
|
|
298
|
-
return undefined;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const aliases: Record<string, ArchiveImportSlice> = Object.assign(
|
|
302
|
-
Object.create(null) as Record<string, ArchiveImportSlice>,
|
|
303
|
-
{
|
|
304
|
-
tweets: "tweets",
|
|
305
|
-
likes: "likes",
|
|
306
|
-
bookmarks: "bookmarks",
|
|
307
|
-
directmessages: "directMessages",
|
|
308
|
-
"direct-messages": "directMessages",
|
|
309
|
-
dms: "directMessages",
|
|
310
|
-
profiles: "profiles",
|
|
311
|
-
followers: "followers",
|
|
312
|
-
following: "following",
|
|
313
|
-
},
|
|
314
|
-
);
|
|
315
|
-
const selected: ArchiveImportSlice[] = [];
|
|
316
|
-
const seen = new Set<ArchiveImportSlice>();
|
|
317
|
-
for (const rawItem of value.split(",")) {
|
|
318
|
-
const item = rawItem.trim();
|
|
319
|
-
if (!item) continue;
|
|
320
|
-
const slice = aliases[item] ?? aliases[item.toLowerCase()];
|
|
321
|
-
if (!slice) {
|
|
322
|
-
printError(
|
|
323
|
-
`--select must be a comma-separated subset of ${ARCHIVE_IMPORT_SLICES.join(", ")}`,
|
|
324
|
-
);
|
|
325
|
-
process.exitCode = 1;
|
|
326
|
-
return undefined;
|
|
327
|
-
}
|
|
328
|
-
if (!seen.has(slice)) {
|
|
329
|
-
seen.add(slice);
|
|
330
|
-
selected.push(slice);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
if (selected.length === 0) {
|
|
335
|
-
printError(
|
|
336
|
-
`--select must include at least one of ${ARCHIVE_IMPORT_SLICES.join(", ")}`,
|
|
337
|
-
);
|
|
338
|
-
process.exitCode = 1;
|
|
339
|
-
return undefined;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
return selected;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function resolveActionOptions(options: { transport?: string }) {
|
|
346
|
-
return {
|
|
347
|
-
transport: options.transport as ActionsTransport | undefined,
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function parseActionsTransport(value: string | undefined) {
|
|
352
|
-
const normalized = value?.trim().toLowerCase();
|
|
353
|
-
if (normalized === "auto" || normalized === "bird" || normalized === "xurl") {
|
|
354
|
-
return normalized;
|
|
355
|
-
}
|
|
356
|
-
printError("transport must be auto, bird, or xurl");
|
|
357
|
-
process.exitCode = 1;
|
|
358
|
-
return undefined;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function parseDigestPeriod(value: string | undefined): PeriodDigestPreset {
|
|
362
|
-
const normalized = value?.trim().toLowerCase();
|
|
363
|
-
if (normalized === "yesterday") return "yesterday";
|
|
364
|
-
if (normalized === "24h" || normalized === "day") return "24h";
|
|
365
|
-
if (normalized === "week" || normalized === "7d") return "week";
|
|
366
|
-
return "today";
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function buildDigestOptions(
|
|
370
|
-
period: string | undefined,
|
|
371
|
-
options: {
|
|
372
|
-
account?: string;
|
|
373
|
-
includeDms?: boolean;
|
|
374
|
-
model?: string;
|
|
375
|
-
language?: string;
|
|
376
|
-
refresh?: boolean;
|
|
377
|
-
since?: string;
|
|
378
|
-
until?: string;
|
|
379
|
-
maxTweets?: string;
|
|
380
|
-
maxLinks?: string;
|
|
381
|
-
liveSync?: boolean;
|
|
382
|
-
liveMode?: string;
|
|
383
|
-
},
|
|
384
|
-
): PeriodDigestOptions | null {
|
|
385
|
-
const maxTweets = parseNonNegativeIntegerOption(
|
|
386
|
-
options.maxTweets,
|
|
387
|
-
"--max-tweets",
|
|
388
|
-
);
|
|
389
|
-
if (options.maxTweets !== undefined && maxTweets === undefined) {
|
|
390
|
-
return null;
|
|
391
|
-
}
|
|
392
|
-
const maxLinks = parseNonNegativeIntegerOption(
|
|
393
|
-
options.maxLinks,
|
|
394
|
-
"--max-links",
|
|
395
|
-
);
|
|
396
|
-
if (options.maxLinks !== undefined && maxLinks === undefined) {
|
|
397
|
-
return null;
|
|
398
|
-
}
|
|
399
|
-
const liveSyncMode = parseDigestLiveModeOption(options.liveMode);
|
|
400
|
-
if (liveSyncMode === undefined) {
|
|
401
|
-
return null;
|
|
402
|
-
}
|
|
403
|
-
let language: string | undefined;
|
|
404
|
-
try {
|
|
405
|
-
language = normalizeDigestLanguage(options.language);
|
|
406
|
-
} catch (error) {
|
|
407
|
-
printError(error instanceof Error ? error.message : String(error));
|
|
408
|
-
process.exitCode = 1;
|
|
409
|
-
return null;
|
|
410
|
-
}
|
|
411
|
-
return {
|
|
412
|
-
period: parseDigestPeriod(period),
|
|
413
|
-
since: options.since,
|
|
414
|
-
until: options.until,
|
|
415
|
-
account: options.account,
|
|
416
|
-
includeDms: Boolean(options.includeDms),
|
|
417
|
-
refresh: Boolean(options.refresh),
|
|
418
|
-
model: options.model,
|
|
419
|
-
language,
|
|
420
|
-
maxTweets,
|
|
421
|
-
maxLinks,
|
|
422
|
-
liveSync: options.liveSync !== false,
|
|
423
|
-
liveSyncMode,
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function runDigestCli(options: PeriodDigestOptions) {
|
|
428
|
-
const asJson = Boolean(program.opts().json);
|
|
429
|
-
return streamPeriodDigest(options, {
|
|
430
|
-
onDelta: asJson
|
|
431
|
-
? undefined
|
|
432
|
-
: (delta) => {
|
|
433
|
-
process.stdout.write(delta);
|
|
434
|
-
},
|
|
435
|
-
}).then((result) => {
|
|
436
|
-
if (asJson) {
|
|
437
|
-
print(result, true);
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
if (!result.markdown.endsWith("\n")) {
|
|
441
|
-
process.stdout.write("\n");
|
|
442
|
-
}
|
|
443
|
-
});
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
function parseSearchDiscussionSource(
|
|
447
|
-
value: string | undefined,
|
|
448
|
-
): SearchDiscussionSource | undefined {
|
|
449
|
-
const normalized = (value ?? "all").trim().toLowerCase();
|
|
450
|
-
if (
|
|
451
|
-
normalized === "all" ||
|
|
452
|
-
normalized === "home" ||
|
|
453
|
-
normalized === "mentions" ||
|
|
454
|
-
normalized === "authored" ||
|
|
455
|
-
normalized === "search" ||
|
|
456
|
-
normalized === "likes" ||
|
|
457
|
-
normalized === "bookmarks"
|
|
458
|
-
) {
|
|
459
|
-
return normalized;
|
|
460
|
-
}
|
|
461
|
-
printError(
|
|
462
|
-
"--source must be all, search, home, mentions, authored, likes, or bookmarks",
|
|
463
|
-
);
|
|
464
|
-
process.exitCode = 1;
|
|
465
|
-
return undefined;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
function parseTweetSearchMode(value: string | undefined) {
|
|
469
|
-
const normalized = (value ?? "auto").trim().toLowerCase();
|
|
470
|
-
if (
|
|
471
|
-
normalized === "auto" ||
|
|
472
|
-
normalized === "bird" ||
|
|
473
|
-
normalized === "xurl" ||
|
|
474
|
-
normalized === "local"
|
|
475
|
-
) {
|
|
476
|
-
return normalized;
|
|
477
|
-
}
|
|
478
|
-
printError("--mode must be auto, bird, xurl, or local");
|
|
479
|
-
process.exitCode = 1;
|
|
480
|
-
return undefined;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
function buildSearchDiscussionOptions(
|
|
484
|
-
query: string,
|
|
485
|
-
options: {
|
|
486
|
-
account?: string;
|
|
487
|
-
source?: string;
|
|
488
|
-
includeDms?: boolean;
|
|
489
|
-
since?: string;
|
|
490
|
-
until?: string;
|
|
491
|
-
question?: string;
|
|
492
|
-
originalsOnly?: boolean;
|
|
493
|
-
hideLowQuality?: boolean;
|
|
494
|
-
mode?: string;
|
|
495
|
-
model?: string;
|
|
496
|
-
refresh?: boolean;
|
|
497
|
-
limit?: string;
|
|
498
|
-
maxPages?: string;
|
|
499
|
-
},
|
|
500
|
-
): SearchDiscussionOptions | null {
|
|
501
|
-
const source = parseSearchDiscussionSource(options.source);
|
|
502
|
-
if (!source) return null;
|
|
503
|
-
const mode = parseTweetSearchMode(options.mode);
|
|
504
|
-
if (!mode) return null;
|
|
505
|
-
const limit = parsePositiveIntegerOption(options.limit, "--limit");
|
|
506
|
-
if (options.limit !== undefined && limit === undefined) {
|
|
507
|
-
return null;
|
|
508
|
-
}
|
|
509
|
-
const maxPages = parsePositiveIntegerOption(options.maxPages, "--max-pages");
|
|
510
|
-
if (options.maxPages !== undefined && maxPages === undefined) {
|
|
511
|
-
return null;
|
|
512
|
-
}
|
|
513
|
-
return {
|
|
514
|
-
query,
|
|
515
|
-
account: options.account,
|
|
516
|
-
source,
|
|
517
|
-
includeDms: Boolean(options.includeDms),
|
|
518
|
-
since: options.since,
|
|
519
|
-
until: options.until,
|
|
520
|
-
question: options.question,
|
|
521
|
-
originalsOnly: Boolean(options.originalsOnly),
|
|
522
|
-
hideLowQuality: Boolean(options.hideLowQuality),
|
|
523
|
-
mode,
|
|
524
|
-
model: options.model,
|
|
525
|
-
refresh: Boolean(options.refresh),
|
|
526
|
-
limit,
|
|
527
|
-
maxPages,
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
function runSearchDiscussionCli(options: SearchDiscussionOptions) {
|
|
532
|
-
const asJson = Boolean(program.opts().json);
|
|
533
|
-
return streamSearchDiscussion(options, {
|
|
534
|
-
onDelta: asJson
|
|
535
|
-
? undefined
|
|
536
|
-
: (delta) => {
|
|
537
|
-
process.stdout.write(delta);
|
|
538
|
-
},
|
|
539
|
-
}).then((result) => {
|
|
540
|
-
if (asJson) {
|
|
541
|
-
print(result, true);
|
|
542
|
-
return;
|
|
543
|
-
}
|
|
544
|
-
if (!result.markdown.endsWith("\n")) {
|
|
545
|
-
process.stdout.write("\n");
|
|
546
|
-
}
|
|
547
|
-
});
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
function buildProfileAnalysisOptions(
|
|
551
|
-
handle: string,
|
|
552
|
-
options: {
|
|
553
|
-
account?: string;
|
|
554
|
-
model?: string;
|
|
555
|
-
refresh?: boolean;
|
|
556
|
-
maxTweets?: string;
|
|
557
|
-
maxPages?: string;
|
|
558
|
-
maxConversations?: string;
|
|
559
|
-
maxConversationPages?: string;
|
|
560
|
-
conversationDelayMs?: string;
|
|
561
|
-
rateLimitRetryMs?: string;
|
|
562
|
-
rateLimitRetries?: string;
|
|
563
|
-
},
|
|
564
|
-
): ProfileAnalysisOptions | null {
|
|
565
|
-
const maxTweets = parsePositiveIntegerOption(
|
|
566
|
-
options.maxTweets,
|
|
567
|
-
"--max-tweets",
|
|
568
|
-
);
|
|
569
|
-
if (options.maxTweets !== undefined && maxTweets === undefined) {
|
|
570
|
-
return null;
|
|
571
|
-
}
|
|
572
|
-
const maxPages = parsePositiveIntegerOption(options.maxPages, "--max-pages");
|
|
573
|
-
if (options.maxPages !== undefined && maxPages === undefined) {
|
|
574
|
-
return null;
|
|
575
|
-
}
|
|
576
|
-
const maxConversations = parsePositiveIntegerOption(
|
|
577
|
-
options.maxConversations,
|
|
578
|
-
"--max-conversations",
|
|
579
|
-
);
|
|
580
|
-
if (
|
|
581
|
-
options.maxConversations !== undefined &&
|
|
582
|
-
maxConversations === undefined
|
|
583
|
-
) {
|
|
584
|
-
return null;
|
|
585
|
-
}
|
|
586
|
-
const maxConversationPages = parsePositiveIntegerOption(
|
|
587
|
-
options.maxConversationPages,
|
|
588
|
-
"--max-conversation-pages",
|
|
589
|
-
);
|
|
590
|
-
if (
|
|
591
|
-
options.maxConversationPages !== undefined &&
|
|
592
|
-
maxConversationPages === undefined
|
|
593
|
-
) {
|
|
594
|
-
return null;
|
|
595
|
-
}
|
|
596
|
-
const conversationDelayMs = parseNonNegativeIntegerOption(
|
|
597
|
-
options.conversationDelayMs,
|
|
598
|
-
"--conversation-delay-ms",
|
|
599
|
-
);
|
|
600
|
-
if (
|
|
601
|
-
options.conversationDelayMs !== undefined &&
|
|
602
|
-
conversationDelayMs === undefined
|
|
603
|
-
) {
|
|
604
|
-
return null;
|
|
605
|
-
}
|
|
606
|
-
const rateLimitRetryMs = parseNonNegativeIntegerOption(
|
|
607
|
-
options.rateLimitRetryMs,
|
|
608
|
-
"--rate-limit-retry-ms",
|
|
609
|
-
);
|
|
610
|
-
if (
|
|
611
|
-
options.rateLimitRetryMs !== undefined &&
|
|
612
|
-
rateLimitRetryMs === undefined
|
|
613
|
-
) {
|
|
614
|
-
return null;
|
|
615
|
-
}
|
|
616
|
-
const rateLimitMaxRetries = parseNonNegativeIntegerOption(
|
|
617
|
-
options.rateLimitRetries,
|
|
618
|
-
"--rate-limit-retries",
|
|
619
|
-
);
|
|
620
|
-
if (
|
|
621
|
-
options.rateLimitRetries !== undefined &&
|
|
622
|
-
rateLimitMaxRetries === undefined
|
|
623
|
-
) {
|
|
624
|
-
return null;
|
|
625
|
-
}
|
|
626
|
-
return {
|
|
627
|
-
handle,
|
|
628
|
-
account: options.account,
|
|
629
|
-
model: options.model,
|
|
630
|
-
refresh: Boolean(options.refresh),
|
|
631
|
-
maxTweets,
|
|
632
|
-
maxPages,
|
|
633
|
-
maxConversations,
|
|
634
|
-
maxConversationPages,
|
|
635
|
-
conversationDelayMs,
|
|
636
|
-
rateLimitRetryMs,
|
|
637
|
-
rateLimitMaxRetries,
|
|
638
|
-
};
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
function runProfileAnalysisCli(options: ProfileAnalysisOptions) {
|
|
642
|
-
const asJson = Boolean(program.opts().json);
|
|
643
|
-
return streamProfileAnalysis(options, {
|
|
644
|
-
onDelta: asJson
|
|
645
|
-
? undefined
|
|
646
|
-
: (delta) => {
|
|
647
|
-
process.stdout.write(delta);
|
|
648
|
-
},
|
|
649
|
-
onEvent: asJson
|
|
650
|
-
? undefined
|
|
651
|
-
: (event) => {
|
|
652
|
-
if (event.type === "status") {
|
|
653
|
-
process.stderr.write(
|
|
654
|
-
event.detail
|
|
655
|
-
? `${event.label}: ${event.detail}\n`
|
|
656
|
-
: `${event.label}\n`,
|
|
657
|
-
);
|
|
658
|
-
}
|
|
659
|
-
},
|
|
660
|
-
}).then((result) => {
|
|
661
|
-
if (asJson) {
|
|
662
|
-
print(result, true);
|
|
663
|
-
return;
|
|
664
|
-
}
|
|
665
|
-
if (!result.markdown.endsWith("\n")) {
|
|
666
|
-
process.stdout.write("\n");
|
|
667
|
-
}
|
|
668
|
-
});
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
async function enrichDmItems(
|
|
672
|
-
query: Parameters<typeof listDmConversations>[0],
|
|
673
|
-
options: {
|
|
674
|
-
resolveProfiles?: boolean;
|
|
675
|
-
expandUrls?: boolean;
|
|
676
|
-
refreshProfileCache?: boolean;
|
|
677
|
-
refreshUrlCache?: boolean;
|
|
678
|
-
xurlFallback?: boolean;
|
|
679
|
-
},
|
|
680
|
-
) {
|
|
681
|
-
let items = listDmConversations(query);
|
|
682
|
-
const profileResolution = options.resolveProfiles
|
|
683
|
-
? await resolveProfilesForIds(
|
|
684
|
-
items.map((item) => item.participant.id),
|
|
685
|
-
{
|
|
686
|
-
refresh: options.refreshProfileCache,
|
|
687
|
-
xurlFallback: options.xurlFallback ?? true,
|
|
688
|
-
},
|
|
689
|
-
)
|
|
690
|
-
: undefined;
|
|
691
|
-
if (profileResolution) {
|
|
692
|
-
items = listDmConversations(query);
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
const urlExpansions = options.expandUrls
|
|
696
|
-
? await expandUrlsFromTexts(
|
|
697
|
-
items.flatMap((item) => [
|
|
698
|
-
item.lastMessagePreview,
|
|
699
|
-
item.searchSnippet ?? "",
|
|
700
|
-
...(item.matches ?? []).flatMap((match) => [
|
|
701
|
-
...match.before.map((message) => message.text),
|
|
702
|
-
match.message.text,
|
|
703
|
-
...match.after.map((message) => message.text),
|
|
704
|
-
]),
|
|
705
|
-
]),
|
|
706
|
-
{ refresh: options.refreshUrlCache },
|
|
707
|
-
)
|
|
708
|
-
: undefined;
|
|
709
|
-
|
|
710
|
-
if (!profileResolution && !urlExpansions) {
|
|
711
|
-
return items;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
return {
|
|
715
|
-
items,
|
|
716
|
-
...(profileResolution ? { profileResolution } : {}),
|
|
717
|
-
...(urlExpansions ? { urlExpansions } : {}),
|
|
718
|
-
};
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
async function autoUpdateBeforeRead() {
|
|
722
|
-
let result: Awaited<ReturnType<typeof maybeAutoUpdateBackup>>;
|
|
723
|
-
try {
|
|
724
|
-
result = await maybeAutoUpdateBackup();
|
|
725
|
-
} catch (error) {
|
|
726
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
727
|
-
console.error(`birdclaw backup auto-sync failed: ${message}`);
|
|
728
|
-
return;
|
|
729
|
-
}
|
|
730
|
-
if (!result.ok) {
|
|
731
|
-
console.error(`birdclaw backup auto-sync failed: ${result.error}`);
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
async function autoSyncAfterWrite() {
|
|
736
|
-
let result: Awaited<ReturnType<typeof maybeAutoSyncBackup>>;
|
|
737
|
-
try {
|
|
738
|
-
result = await maybeAutoSyncBackup();
|
|
739
|
-
} catch (error) {
|
|
740
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
741
|
-
console.error(`birdclaw backup sync failed: ${message}`);
|
|
742
|
-
return;
|
|
743
|
-
}
|
|
744
|
-
if (!result.ok) {
|
|
745
|
-
console.error(`birdclaw backup sync failed: ${result.error}`);
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
program
|
|
750
|
-
.name("birdclaw")
|
|
751
|
-
.description("Local-first Twitter workspace")
|
|
752
|
-
.version(packageVersion.version ?? "0.0.0")
|
|
753
|
-
.option("--json", "Emit JSON output");
|
|
754
|
-
|
|
755
|
-
program
|
|
756
|
-
.command("init")
|
|
757
|
-
.description("Create local birdclaw root and seed the database")
|
|
758
|
-
.action(async () => {
|
|
759
|
-
const paths = ensureBirdclawDirs();
|
|
760
|
-
await getQueryEnvelope();
|
|
761
|
-
print(
|
|
762
|
-
{
|
|
763
|
-
ok: true,
|
|
764
|
-
rootDir: paths.rootDir,
|
|
765
|
-
configPath: paths.configPath,
|
|
766
|
-
dbPath: paths.dbPath,
|
|
767
|
-
mediaOriginalsDir: paths.mediaOriginalsDir,
|
|
768
|
-
mediaThumbsDir: paths.mediaThumbsDir,
|
|
769
|
-
},
|
|
770
|
-
program.opts().json ?? false,
|
|
771
|
-
);
|
|
772
|
-
});
|
|
773
|
-
|
|
774
|
-
const authCommand = program
|
|
775
|
-
.command("auth")
|
|
776
|
-
.description("Manage live transport");
|
|
777
|
-
|
|
778
|
-
authCommand
|
|
779
|
-
.command("status")
|
|
780
|
-
.description("Show transport status")
|
|
781
|
-
.action(async () => {
|
|
782
|
-
const meta = await getQueryEnvelope();
|
|
783
|
-
print(meta.transport, program.opts().json ?? false);
|
|
784
|
-
});
|
|
785
|
-
|
|
786
|
-
authCommand
|
|
787
|
-
.command("use <transport>")
|
|
788
|
-
.description("Set preferred moderation action transport")
|
|
789
|
-
.action((transport: string) => {
|
|
790
|
-
const parsed = parseActionsTransport(transport);
|
|
791
|
-
if (!parsed) return;
|
|
792
|
-
print(setActionsTransport(parsed), program.opts().json ?? false);
|
|
793
|
-
});
|
|
794
|
-
|
|
795
|
-
program
|
|
796
|
-
.command("archive find")
|
|
797
|
-
.description("Find likely Twitter archives on disk")
|
|
798
|
-
.action(async () => {
|
|
799
|
-
const items = await findArchives();
|
|
800
|
-
print(items, program.opts().json ?? false);
|
|
801
|
-
});
|
|
802
|
-
|
|
803
|
-
const importCommand = program
|
|
804
|
-
.command("import")
|
|
805
|
-
.description("Import local archive data");
|
|
806
|
-
|
|
807
|
-
importCommand
|
|
808
|
-
.command("archive [archivePath]")
|
|
809
|
-
.description("Import a Twitter archive into the local SQLite store")
|
|
810
|
-
.option(
|
|
811
|
-
"--select <kinds>",
|
|
812
|
-
`Import only selected archive slices: ${ARCHIVE_IMPORT_SLICES.join(", ")}`,
|
|
813
|
-
)
|
|
814
|
-
.action(async (archivePath, options: { select?: string }) => {
|
|
815
|
-
const select = parseArchiveImportSelect(options.select);
|
|
816
|
-
if (options.select !== undefined && !select) {
|
|
817
|
-
return;
|
|
818
|
-
}
|
|
819
|
-
let resolvedArchivePath = archivePath;
|
|
820
|
-
if (!resolvedArchivePath) {
|
|
821
|
-
const [latestArchive] = await findArchives();
|
|
822
|
-
resolvedArchivePath = latestArchive?.path;
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
if (!resolvedArchivePath) {
|
|
826
|
-
throw new Error(
|
|
827
|
-
"No archive found. Pass a path or place one in Downloads.",
|
|
828
|
-
);
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
const asJson = Boolean(program.opts().json);
|
|
832
|
-
const result = await importArchive(resolvedArchivePath, {
|
|
833
|
-
select,
|
|
834
|
-
onProgress: asJson ? undefined : logImportProgress,
|
|
835
|
-
});
|
|
836
|
-
await autoSyncAfterWrite();
|
|
837
|
-
print(result, asJson);
|
|
838
|
-
});
|
|
839
|
-
|
|
840
|
-
importCommand
|
|
841
|
-
.command("hydrate-profiles")
|
|
842
|
-
.description("Backfill archive-imported profiles from live Twitter metadata")
|
|
843
|
-
.action(async () => {
|
|
844
|
-
const result = await hydrateProfilesFromX();
|
|
845
|
-
await autoSyncAfterWrite();
|
|
846
|
-
print(result, program.opts().json ?? false);
|
|
847
|
-
});
|
|
848
|
-
|
|
849
|
-
const searchCommand = program
|
|
850
|
-
.command("search")
|
|
851
|
-
.description("Search local data");
|
|
852
|
-
|
|
853
|
-
searchCommand
|
|
854
|
-
.command("tweets [query]")
|
|
855
|
-
.option("--resource <resource>", "home, mentions, or authored", "home")
|
|
856
|
-
.option("--replied", "Only replied items")
|
|
857
|
-
.option("--unreplied", "Only unreplied items")
|
|
858
|
-
.option("--since <date>", "Include tweets created at or after this date")
|
|
859
|
-
.option("--until <date>", "Include tweets created before this date")
|
|
860
|
-
.option("--originals-only", "Exclude authored replies that start with @")
|
|
861
|
-
.option("--hide-low-quality", "Hide RTs, tiny replies, and link-only noise")
|
|
862
|
-
.option(
|
|
863
|
-
"--min-likes <n>",
|
|
864
|
-
"Override the low-quality like threshold (default 50)",
|
|
865
|
-
)
|
|
866
|
-
.option("--quality-reason", "Include qualityReason on each row")
|
|
867
|
-
.option("--liked", "Only liked tweets")
|
|
868
|
-
.option("--bookmarked", "Only bookmarked tweets")
|
|
869
|
-
.option("--limit <n>", "Limit results", "20")
|
|
870
|
-
.action(async (query, options) => {
|
|
871
|
-
const minLikes = parseNonNegativeIntegerOption(
|
|
872
|
-
options.minLikes,
|
|
873
|
-
"--min-likes",
|
|
874
|
-
);
|
|
875
|
-
if (options.minLikes !== undefined && minLikes === undefined) {
|
|
876
|
-
return;
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
await autoUpdateBeforeRead();
|
|
880
|
-
const replyFilter = options.replied
|
|
881
|
-
? "replied"
|
|
882
|
-
: options.unreplied
|
|
883
|
-
? "unreplied"
|
|
884
|
-
: "all";
|
|
885
|
-
const items = listTimelineItems({
|
|
886
|
-
resource:
|
|
887
|
-
options.resource === "mentions"
|
|
888
|
-
? "mentions"
|
|
889
|
-
: options.resource === "authored"
|
|
890
|
-
? "authored"
|
|
891
|
-
: "home",
|
|
892
|
-
search: query,
|
|
893
|
-
replyFilter,
|
|
894
|
-
since: options.since,
|
|
895
|
-
until: options.until,
|
|
896
|
-
includeReplies: !options.originalsOnly,
|
|
897
|
-
qualityFilter: options.hideLowQuality ? "summary" : "all",
|
|
898
|
-
lowQualityThreshold: minLikes,
|
|
899
|
-
includeQualityReason: Boolean(options.qualityReason),
|
|
900
|
-
likedOnly: Boolean(options.liked),
|
|
901
|
-
bookmarkedOnly: Boolean(options.bookmarked),
|
|
902
|
-
limit: Number(options.limit),
|
|
903
|
-
});
|
|
904
|
-
print(items, program.opts().json ?? false);
|
|
905
|
-
});
|
|
906
|
-
|
|
907
|
-
searchCommand
|
|
908
|
-
.command("dms <query>")
|
|
909
|
-
.option("--inbox <kind>", "all, accepted, or requests", "all")
|
|
910
|
-
.option("--participant <value>")
|
|
911
|
-
.option("--min-followers <n>", "Minimum sender follower count")
|
|
912
|
-
.option("--max-followers <n>", "Maximum sender follower count")
|
|
913
|
-
.option("--min-influence-score <n>", "Minimum derived influence score")
|
|
914
|
-
.option("--max-influence-score <n>", "Maximum derived influence score")
|
|
915
|
-
.option("--sort <mode>", "recent or followers", "recent")
|
|
916
|
-
.option(
|
|
917
|
-
"--context <n>",
|
|
918
|
-
"Include N messages before and after each match",
|
|
919
|
-
"0",
|
|
920
|
-
)
|
|
921
|
-
.option(
|
|
922
|
-
"--resolve-profiles",
|
|
923
|
-
"Resolve placeholder DM profiles through cache/bird/xurl",
|
|
924
|
-
)
|
|
925
|
-
.option("--expand-urls", "Expand URLs through the persistent URL cache")
|
|
926
|
-
.option("--refresh-profile-cache", "Bypass profile lookup cache")
|
|
927
|
-
.option("--refresh-url-cache", "Bypass URL expansion cache")
|
|
928
|
-
.option(
|
|
929
|
-
"--no-xurl-fallback",
|
|
930
|
-
"Do not fall back to xurl after bird profile lookup",
|
|
931
|
-
)
|
|
932
|
-
.option("--replied", "Only replied threads")
|
|
933
|
-
.option("--unreplied", "Only unreplied threads")
|
|
934
|
-
.option("--limit <n>", "Limit results", "20")
|
|
935
|
-
.action(async (query, options) => {
|
|
936
|
-
await autoUpdateBeforeRead();
|
|
937
|
-
const context = parseNonNegativeIntegerOption(options.context, "--context");
|
|
938
|
-
if (context === undefined) {
|
|
939
|
-
return;
|
|
940
|
-
}
|
|
941
|
-
const inbox = parseDmInboxOption(options.inbox);
|
|
942
|
-
if (inbox === undefined) {
|
|
943
|
-
return;
|
|
944
|
-
}
|
|
945
|
-
const replyFilter = options.replied
|
|
946
|
-
? "replied"
|
|
947
|
-
: options.unreplied
|
|
948
|
-
? "unreplied"
|
|
949
|
-
: "all";
|
|
950
|
-
const dmQuery = {
|
|
951
|
-
search: query,
|
|
952
|
-
...(inbox !== "all" ? { inbox } : {}),
|
|
953
|
-
participant: options.participant,
|
|
954
|
-
minFollowers: options.minFollowers
|
|
955
|
-
? Number(options.minFollowers)
|
|
956
|
-
: undefined,
|
|
957
|
-
maxFollowers: options.maxFollowers
|
|
958
|
-
? Number(options.maxFollowers)
|
|
959
|
-
: undefined,
|
|
960
|
-
minInfluenceScore: options.minInfluenceScore
|
|
961
|
-
? Number(options.minInfluenceScore)
|
|
962
|
-
: undefined,
|
|
963
|
-
maxInfluenceScore: options.maxInfluenceScore
|
|
964
|
-
? Number(options.maxInfluenceScore)
|
|
965
|
-
: undefined,
|
|
966
|
-
sort:
|
|
967
|
-
options.sort === "followers" || options.sort === "influence"
|
|
968
|
-
? "followers"
|
|
969
|
-
: "recent",
|
|
970
|
-
replyFilter,
|
|
971
|
-
context,
|
|
972
|
-
limit: Number(options.limit),
|
|
973
|
-
} as const;
|
|
974
|
-
const items = await enrichDmItems(dmQuery, {
|
|
975
|
-
resolveProfiles: Boolean(options.resolveProfiles),
|
|
976
|
-
expandUrls: Boolean(options.expandUrls),
|
|
977
|
-
refreshProfileCache: Boolean(options.refreshProfileCache),
|
|
978
|
-
refreshUrlCache: Boolean(options.refreshUrlCache),
|
|
979
|
-
xurlFallback: options.xurlFallback,
|
|
980
|
-
});
|
|
981
|
-
print(items, program.opts().json ?? false);
|
|
982
|
-
});
|
|
983
|
-
|
|
984
|
-
searchCommand
|
|
985
|
-
.command("links <query>")
|
|
986
|
-
.description("Search indexed short links, expansions, and linked tweets")
|
|
987
|
-
.option("--account <accountIdOrHandle>", "Account id or handle")
|
|
988
|
-
.option("--since <date>", "Include links created at or after this date")
|
|
989
|
-
.option("--until <date>", "Include links created before this date")
|
|
990
|
-
.option("--source <kind>", "dm or tweet")
|
|
991
|
-
.option("--direction <direction>", "inbound or outbound")
|
|
992
|
-
.option("--participant <value>", "DM participant handle or name")
|
|
993
|
-
.option("--media <type>", "image, video, or gif")
|
|
994
|
-
.option("--limit <n>", "Limit results", "20")
|
|
995
|
-
.action(async (query, options) => {
|
|
996
|
-
await autoUpdateBeforeRead();
|
|
997
|
-
const items = searchLinks(query, {
|
|
998
|
-
account: options.account,
|
|
999
|
-
since: options.since,
|
|
1000
|
-
until: options.until,
|
|
1001
|
-
source:
|
|
1002
|
-
options.source === "tweet"
|
|
1003
|
-
? "tweet"
|
|
1004
|
-
: options.source === "dm"
|
|
1005
|
-
? "dm"
|
|
1006
|
-
: undefined,
|
|
1007
|
-
direction:
|
|
1008
|
-
options.direction === "inbound"
|
|
1009
|
-
? "inbound"
|
|
1010
|
-
: options.direction === "outbound"
|
|
1011
|
-
? "outbound"
|
|
1012
|
-
: undefined,
|
|
1013
|
-
participant: options.participant,
|
|
1014
|
-
mediaType:
|
|
1015
|
-
options.media === "image" ||
|
|
1016
|
-
options.media === "video" ||
|
|
1017
|
-
options.media === "gif"
|
|
1018
|
-
? options.media
|
|
1019
|
-
: undefined,
|
|
1020
|
-
limit: Number(options.limit),
|
|
1021
|
-
});
|
|
1022
|
-
if (program.opts().json) {
|
|
1023
|
-
print(items, true);
|
|
1024
|
-
return;
|
|
1025
|
-
}
|
|
1026
|
-
console.log(formatLinkSearchItems(items));
|
|
1027
|
-
});
|
|
1028
|
-
|
|
1029
|
-
const linksCommand = program
|
|
1030
|
-
.command("links")
|
|
1031
|
-
.description("Build and inspect the short-link index");
|
|
1032
|
-
|
|
1033
|
-
linksCommand
|
|
1034
|
-
.command("backfill")
|
|
1035
|
-
.description("Backfill indexed URL occurrences and t.co expansions")
|
|
1036
|
-
.option("--all-urls", "Index all URLs, not only t.co")
|
|
1037
|
-
.option("--source <kind>", "dm or tweet")
|
|
1038
|
-
.option("--refresh-url-cache", "Re-expand URLs already in the index")
|
|
1039
|
-
.option("--limit <n>", "Limit network/cache expansions for this run")
|
|
1040
|
-
.option("--concurrency <n>", "Concurrent URL expansion workers", "12")
|
|
1041
|
-
.option("--timeout-ms <n>", "Per-redirect fetch timeout", "15000")
|
|
1042
|
-
.action(async (options) => {
|
|
1043
|
-
const limit = parseNonNegativeIntegerOption(options.limit, "--limit");
|
|
1044
|
-
if (options.limit !== undefined && limit === undefined) {
|
|
1045
|
-
return;
|
|
1046
|
-
}
|
|
1047
|
-
const concurrency = parseNonNegativeIntegerOption(
|
|
1048
|
-
options.concurrency,
|
|
1049
|
-
"--concurrency",
|
|
1050
|
-
);
|
|
1051
|
-
if (concurrency === undefined) {
|
|
1052
|
-
return;
|
|
1053
|
-
}
|
|
1054
|
-
const timeoutMs = parseNonNegativeIntegerOption(
|
|
1055
|
-
options.timeoutMs,
|
|
1056
|
-
"--timeout-ms",
|
|
1057
|
-
);
|
|
1058
|
-
if (timeoutMs === undefined) {
|
|
1059
|
-
return;
|
|
1060
|
-
}
|
|
1061
|
-
const result = await backfillLinkIndex({
|
|
1062
|
-
includeAllUrls: Boolean(options.allUrls),
|
|
1063
|
-
refresh: Boolean(options.refreshUrlCache),
|
|
1064
|
-
source:
|
|
1065
|
-
options.source === "tweet"
|
|
1066
|
-
? "tweet"
|
|
1067
|
-
: options.source === "dm"
|
|
1068
|
-
? "dm"
|
|
1069
|
-
: undefined,
|
|
1070
|
-
limit,
|
|
1071
|
-
concurrency,
|
|
1072
|
-
timeoutMs,
|
|
1073
|
-
});
|
|
1074
|
-
await autoSyncAfterWrite();
|
|
1075
|
-
print(result, program.opts().json ?? false);
|
|
1076
|
-
});
|
|
1077
|
-
|
|
1078
|
-
const mediaCommand = program
|
|
1079
|
-
.command("media")
|
|
1080
|
-
.description("Manage the local media cache");
|
|
1081
|
-
|
|
1082
|
-
mediaCommand
|
|
1083
|
-
.command("fetch")
|
|
1084
|
-
.description(
|
|
1085
|
-
"Fetch missing pbs.twimg.com image media already stored in tweets",
|
|
1086
|
-
)
|
|
1087
|
-
.option("--account <accountId>", "Account id")
|
|
1088
|
-
.option("--limit <n>", "Stop after N tweets processed")
|
|
1089
|
-
.option(
|
|
1090
|
-
"--kind <kind>",
|
|
1091
|
-
"Tweet or collection kind, e.g. home, like, bookmark",
|
|
1092
|
-
)
|
|
1093
|
-
.option("--since <isoDate>", "Only tweets created at or after this date")
|
|
1094
|
-
.option("--parallel <n>", "Concurrent fetch workers, capped at 5", "1")
|
|
1095
|
-
.option("--pacing-ms <n>", "Delay between request starts", "250")
|
|
1096
|
-
.option("--video-pacing-ms <n>", "Delay between video request starts")
|
|
1097
|
-
.option("--retry-max <n>", "Retries per file after rate limiting", "3")
|
|
1098
|
-
.option("--include-video", "Include video and animated GIF media", true)
|
|
1099
|
-
.option("--no-include-video", "Skip video and animated GIF media")
|
|
1100
|
-
.option("--max-bytes <n>", "Maximum media file size in bytes", "104857600")
|
|
1101
|
-
.option("--dry-run", "List what would be fetched without downloading")
|
|
1102
|
-
.option("--json", "Emit JSON output")
|
|
1103
|
-
.action(async (options) => {
|
|
1104
|
-
const limit = parseNonNegativeIntegerOption(options.limit, "--limit");
|
|
1105
|
-
if (options.limit !== undefined && limit === undefined) {
|
|
1106
|
-
return;
|
|
1107
|
-
}
|
|
1108
|
-
const parallel =
|
|
1109
|
-
parsePositiveIntegerOption(options.parallel, "--parallel") ?? 1;
|
|
1110
|
-
const pacingMs =
|
|
1111
|
-
parseNonNegativeIntegerOption(options.pacingMs, "--pacing-ms") ?? 250;
|
|
1112
|
-
const retryMax =
|
|
1113
|
-
parseNonNegativeIntegerOption(options.retryMax, "--retry-max") ?? 3;
|
|
1114
|
-
const videoPacingMs =
|
|
1115
|
-
options.videoPacingMs === undefined
|
|
1116
|
-
? undefined
|
|
1117
|
-
: parseNonNegativeIntegerOption(
|
|
1118
|
-
options.videoPacingMs,
|
|
1119
|
-
"--video-pacing-ms",
|
|
1120
|
-
);
|
|
1121
|
-
const maxBytes =
|
|
1122
|
-
parseNonNegativeIntegerOption(options.maxBytes, "--max-bytes") ??
|
|
1123
|
-
100 * 1024 * 1024;
|
|
1124
|
-
if (process.exitCode) {
|
|
1125
|
-
return;
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
const result = await fetchTweetMedia({
|
|
1129
|
-
account: options.account,
|
|
1130
|
-
limit,
|
|
1131
|
-
kind: options.kind,
|
|
1132
|
-
since: options.since,
|
|
1133
|
-
parallel,
|
|
1134
|
-
pacingMs,
|
|
1135
|
-
videoPacingMs,
|
|
1136
|
-
retryMax,
|
|
1137
|
-
includeVideo: Boolean(options.includeVideo),
|
|
1138
|
-
maxBytes,
|
|
1139
|
-
dryRun: Boolean(options.dryRun),
|
|
1140
|
-
});
|
|
1141
|
-
const asJson = Boolean(program.opts().json || options.json);
|
|
1142
|
-
print(asJson ? result : formatMediaFetchResult(result), asJson);
|
|
1143
|
-
});
|
|
1144
|
-
|
|
1145
|
-
program
|
|
1146
|
-
.command("whois <query>")
|
|
1147
|
-
.description("Identify likely people or orgs from local DMs and tweets")
|
|
1148
|
-
.option("--account <accountId>", "Account id")
|
|
1149
|
-
.option("--no-dms", "Do not search DMs")
|
|
1150
|
-
.option("--tweets", "Include local tweet search evidence")
|
|
1151
|
-
.option("--no-resolve-profiles", "Do not resolve placeholder profiles")
|
|
1152
|
-
.option("--no-expand-urls", "Do not expand URLs")
|
|
1153
|
-
.option("--refresh-profile-cache", "Bypass profile lookup cache")
|
|
1154
|
-
.option("--refresh-url-cache", "Bypass URL expansion cache")
|
|
1155
|
-
.option(
|
|
1156
|
-
"--no-xurl-fallback",
|
|
1157
|
-
"Do not fall back to xurl after bird profile lookup",
|
|
1158
|
-
)
|
|
1159
|
-
.option(
|
|
1160
|
-
"--affiliation <query>",
|
|
1161
|
-
"Require affiliation, bio, or history evidence",
|
|
1162
|
-
)
|
|
1163
|
-
.option(
|
|
1164
|
-
"--current-affiliation <query>",
|
|
1165
|
-
"Require an active affiliation badge",
|
|
1166
|
-
)
|
|
1167
|
-
.option(
|
|
1168
|
-
"--exclude-domain-only",
|
|
1169
|
-
"Drop candidates that only match domains/URLs",
|
|
1170
|
-
)
|
|
1171
|
-
.option("--context <n>", "DM messages before and after each match", "4")
|
|
1172
|
-
.option("--limit <n>", "Limit candidates", "10")
|
|
1173
|
-
.action(async (query, options) => {
|
|
1174
|
-
await autoUpdateBeforeRead();
|
|
1175
|
-
const context = parseNonNegativeIntegerOption(options.context, "--context");
|
|
1176
|
-
if (context === undefined) {
|
|
1177
|
-
return;
|
|
1178
|
-
}
|
|
1179
|
-
const result = await runWhois(query, {
|
|
1180
|
-
account: options.account,
|
|
1181
|
-
dms: options.dms,
|
|
1182
|
-
tweets: Boolean(options.tweets),
|
|
1183
|
-
resolveProfiles: options.resolveProfiles,
|
|
1184
|
-
expandUrls: options.expandUrls,
|
|
1185
|
-
refreshProfileCache: Boolean(options.refreshProfileCache),
|
|
1186
|
-
refreshUrlCache: Boolean(options.refreshUrlCache),
|
|
1187
|
-
xurlFallback: options.xurlFallback,
|
|
1188
|
-
affiliation: options.affiliation,
|
|
1189
|
-
currentAffiliation: options.currentAffiliation,
|
|
1190
|
-
excludeDomainOnly: Boolean(options.excludeDomainOnly),
|
|
1191
|
-
context,
|
|
1192
|
-
limit: Number(options.limit),
|
|
1193
|
-
});
|
|
1194
|
-
print(
|
|
1195
|
-
program.opts().json ? result : formatWhois(result),
|
|
1196
|
-
program.opts().json ?? false,
|
|
1197
|
-
);
|
|
1198
|
-
});
|
|
1199
|
-
|
|
1200
|
-
program
|
|
1201
|
-
.command("research [query]")
|
|
1202
|
-
.description("Build a markdown research brief from bookmarked threads")
|
|
1203
|
-
.option("--account <accountId>", "Account id")
|
|
1204
|
-
.option("--limit <n>", "Seed bookmark limit", "20")
|
|
1205
|
-
.option("--thread-depth <n>", "Maximum ancestor walk depth", "10")
|
|
1206
|
-
.option("--out <path>", "Write the markdown brief to a file")
|
|
1207
|
-
.action(async (query, options) => {
|
|
1208
|
-
await autoUpdateBeforeRead();
|
|
1209
|
-
const report = await runResearchMode({
|
|
1210
|
-
account: options.account,
|
|
1211
|
-
query,
|
|
1212
|
-
limit: Number(options.limit),
|
|
1213
|
-
maxThreadDepth: Number(options.threadDepth),
|
|
1214
|
-
outPath: options.out,
|
|
1215
|
-
});
|
|
1216
|
-
print(
|
|
1217
|
-
program.opts().json ? report : report.markdown,
|
|
1218
|
-
program.opts().json ?? false,
|
|
1219
|
-
);
|
|
1220
|
-
});
|
|
1221
|
-
|
|
1222
|
-
program
|
|
1223
|
-
.command("discuss <query>")
|
|
1224
|
-
.description("Search live/local tweets and summarize the results with AI")
|
|
1225
|
-
.option("--account <accountId>", "Account id")
|
|
1226
|
-
.option(
|
|
1227
|
-
"--source <source>",
|
|
1228
|
-
"all, search, home, mentions, authored, likes, or bookmarks",
|
|
1229
|
-
"search",
|
|
1230
|
-
)
|
|
1231
|
-
.option("--mode <mode>", "auto, bird, xurl, or local", "xurl")
|
|
1232
|
-
.option("--include-dms", "Include private DM search matches")
|
|
1233
|
-
.option("--since <isoDate>", "Include matches created at or after this date")
|
|
1234
|
-
.option("--until <isoDate>", "Include matches created before this date")
|
|
1235
|
-
.option("--question <prompt>", "Discussion question or angle")
|
|
1236
|
-
.option("--originals-only", "Exclude authored replies that start with @")
|
|
1237
|
-
.option("--hide-low-quality", "Hide RTs, tiny replies, and link-only noise")
|
|
1238
|
-
.option("--model <model>", "OpenAI model id")
|
|
1239
|
-
.option("--refresh", "Bypass the local discussion cache")
|
|
1240
|
-
.option("--limit <n>", "Maximum tweet context", "20000")
|
|
1241
|
-
.option("--max-pages <n>", "Maximum live search pages", "200")
|
|
1242
|
-
.action(async (query, options) => {
|
|
1243
|
-
await autoUpdateBeforeRead();
|
|
1244
|
-
const discussionOptions = buildSearchDiscussionOptions(query, options);
|
|
1245
|
-
if (!discussionOptions) return;
|
|
1246
|
-
await runSearchDiscussionCli(discussionOptions);
|
|
1247
|
-
});
|
|
1248
|
-
|
|
1249
|
-
program
|
|
1250
|
-
.command("profile-analyze <handle>")
|
|
1251
|
-
.alias("profile-analyse")
|
|
1252
|
-
.description("Backfill a profile with xurl and summarize it with AI")
|
|
1253
|
-
.option("--account <accountId>", "Account id")
|
|
1254
|
-
.option("--model <model>", "OpenAI model id")
|
|
1255
|
-
.option("--refresh", "Bypass profile fetch and analysis caches")
|
|
1256
|
-
.option("--max-tweets <n>", "Maximum profile tweets", "10000")
|
|
1257
|
-
.option("--max-pages <n>", "Maximum profile timeline pages", "100")
|
|
1258
|
-
.option("--max-conversations <n>", "Maximum conversations to backfill", "80")
|
|
1259
|
-
.option("--max-conversation-pages <n>", "Maximum pages per conversation", "3")
|
|
1260
|
-
.option(
|
|
1261
|
-
"--conversation-delay-ms <n>",
|
|
1262
|
-
"Delay between conversation search calls",
|
|
1263
|
-
)
|
|
1264
|
-
.option(
|
|
1265
|
-
"--rate-limit-retry-ms <n>",
|
|
1266
|
-
"Delay before retrying conversation 429s",
|
|
1267
|
-
)
|
|
1268
|
-
.option("--rate-limit-retries <n>", "Conversation 429 retry count")
|
|
1269
|
-
.action(async (handle, options) => {
|
|
1270
|
-
await autoUpdateBeforeRead();
|
|
1271
|
-
const analysisOptions = buildProfileAnalysisOptions(handle, options);
|
|
1272
|
-
if (!analysisOptions) return;
|
|
1273
|
-
await runProfileAnalysisCli(analysisOptions);
|
|
1274
|
-
});
|
|
1275
|
-
|
|
1276
|
-
program
|
|
1277
|
-
.command("today")
|
|
1278
|
-
.description("Stream an AI digest of what happened today")
|
|
1279
|
-
.option("--account <accountId>", "Account id")
|
|
1280
|
-
.option("--include-dms", "Include private DM context")
|
|
1281
|
-
.option("--model <model>", "OpenAI model id")
|
|
1282
|
-
.option(
|
|
1283
|
-
"--language <tag>",
|
|
1284
|
-
"Report language as a Unicode locale id, e.g. zh-CN (env: BIRDCLAW_DIGEST_LANGUAGE)",
|
|
1285
|
-
)
|
|
1286
|
-
.option("--refresh", "Bypass the local digest cache")
|
|
1287
|
-
.option("--max-tweets <n>", "Maximum tweet context", "5000")
|
|
1288
|
-
.option("--max-links <n>", "Maximum linked articles", "12")
|
|
1289
|
-
.option("--no-live-sync", "Use only the local database")
|
|
1290
|
-
.option(
|
|
1291
|
-
"--live-mode <mode>",
|
|
1292
|
-
"Live timeline mode: xurl, bird, or auto",
|
|
1293
|
-
"xurl",
|
|
1294
|
-
)
|
|
1295
|
-
.action(async (options) => {
|
|
1296
|
-
await autoUpdateBeforeRead();
|
|
1297
|
-
const digestOptions = buildDigestOptions("today", options);
|
|
1298
|
-
if (!digestOptions) return;
|
|
1299
|
-
await runDigestCli(digestOptions);
|
|
1300
|
-
});
|
|
1301
|
-
|
|
1302
|
-
program
|
|
1303
|
-
.command("digest [period]")
|
|
1304
|
-
.description("Stream an AI digest for today, 24h, yesterday, or week")
|
|
1305
|
-
.option("--account <accountId>", "Account id")
|
|
1306
|
-
.option("--include-dms", "Include private DM context")
|
|
1307
|
-
.option("--since <isoDate>", "Start of explicit window")
|
|
1308
|
-
.option("--until <isoDate>", "End of explicit window")
|
|
1309
|
-
.option("--model <model>", "OpenAI model id")
|
|
1310
|
-
.option(
|
|
1311
|
-
"--language <tag>",
|
|
1312
|
-
"Report language as a Unicode locale id, e.g. zh-CN (env: BIRDCLAW_DIGEST_LANGUAGE)",
|
|
1313
|
-
)
|
|
1314
|
-
.option("--refresh", "Bypass the local digest cache")
|
|
1315
|
-
.option("--max-tweets <n>", "Maximum tweet context", "5000")
|
|
1316
|
-
.option("--max-links <n>", "Maximum linked articles", "12")
|
|
1317
|
-
.option("--no-live-sync", "Use only the local database")
|
|
1318
|
-
.option(
|
|
1319
|
-
"--live-mode <mode>",
|
|
1320
|
-
"Live timeline mode: xurl, bird, or auto",
|
|
1321
|
-
"xurl",
|
|
1322
|
-
)
|
|
1323
|
-
.action(async (period, options) => {
|
|
1324
|
-
await autoUpdateBeforeRead();
|
|
1325
|
-
const digestOptions = buildDigestOptions(period, options);
|
|
1326
|
-
if (!digestOptions) return;
|
|
1327
|
-
await runDigestCli(digestOptions);
|
|
1328
|
-
});
|
|
1329
|
-
|
|
1330
|
-
const mentionsCommand = program
|
|
1331
|
-
.command("mentions")
|
|
1332
|
-
.description("Export local mention tweets for scripts and agents");
|
|
1333
|
-
|
|
1334
|
-
mentionsCommand
|
|
1335
|
-
.command("export [query]")
|
|
1336
|
-
.description("Return mention tweets with plain-text and markdown renderings")
|
|
1337
|
-
.option("--account <accountId>", "Account id")
|
|
1338
|
-
.option("--mode <mode>", "birdclaw, auto, xurl, or bird")
|
|
1339
|
-
.option("--replied", "Only replied items")
|
|
1340
|
-
.option("--unreplied", "Only unreplied items")
|
|
1341
|
-
.option("--refresh", "Refresh the live xurl cache before returning")
|
|
1342
|
-
.option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
|
|
1343
|
-
.option("--all", "Fetch every retrievable xurl mentions page")
|
|
1344
|
-
.option(
|
|
1345
|
-
"--max-pages <n>",
|
|
1346
|
-
"Maximum xurl mention pages to fetch (implies --all)",
|
|
1347
|
-
)
|
|
1348
|
-
.option("--limit <n>", "Limit results", "20")
|
|
1349
|
-
.action(async (query, options) => {
|
|
1350
|
-
await autoUpdateBeforeRead();
|
|
1351
|
-
const replyFilter = options.replied
|
|
1352
|
-
? "replied"
|
|
1353
|
-
: options.unreplied
|
|
1354
|
-
? "unreplied"
|
|
1355
|
-
: "all";
|
|
1356
|
-
const limit = Number(options.limit);
|
|
1357
|
-
const mode = resolveMentionsDataSource(options.mode);
|
|
1358
|
-
if (mode === "xurl" || mode === "bird" || mode === "auto") {
|
|
1359
|
-
const exportFn =
|
|
1360
|
-
mode === "xurl"
|
|
1361
|
-
? exportMentionsViaCachedXurl
|
|
1362
|
-
: mode === "bird"
|
|
1363
|
-
? exportMentionsViaCachedBird
|
|
1364
|
-
: exportMentionsViaCachedAuto;
|
|
1365
|
-
const payload = await exportFn({
|
|
1366
|
-
account: options.account,
|
|
1367
|
-
search: query,
|
|
1368
|
-
replyFilter,
|
|
1369
|
-
limit,
|
|
1370
|
-
all: Boolean(options.all) || options.maxPages !== undefined,
|
|
1371
|
-
maxPages: options.maxPages ? Number(options.maxPages) : undefined,
|
|
1372
|
-
refresh: Boolean(options.refresh),
|
|
1373
|
-
cacheTtlMs: Number(options.cacheTtl) * 1000,
|
|
1374
|
-
});
|
|
1375
|
-
await autoSyncAfterWrite();
|
|
1376
|
-
print(payload, true);
|
|
1377
|
-
return;
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
const items = exportMentionItems({
|
|
1381
|
-
account: options.account,
|
|
1382
|
-
search: query,
|
|
1383
|
-
replyFilter,
|
|
1384
|
-
limit,
|
|
1385
|
-
});
|
|
1386
|
-
print({ resource: "mentions", count: items.length, items }, true);
|
|
1387
|
-
});
|
|
1388
|
-
|
|
1389
|
-
const profilesCommand = program
|
|
1390
|
-
.command("profiles")
|
|
1391
|
-
.description("Inspect live profile context for moderation and triage");
|
|
1392
|
-
|
|
1393
|
-
profilesCommand
|
|
1394
|
-
.command("replies <query>")
|
|
1395
|
-
.description("Inspect recent authored replies for one profile")
|
|
1396
|
-
.option("--limit <n>", "Limit replies", "12")
|
|
1397
|
-
.action(async (query, options) => {
|
|
1398
|
-
const result = await inspectProfileReplies(query, {
|
|
1399
|
-
limit: Number(options.limit),
|
|
1400
|
-
});
|
|
1401
|
-
print(result, program.opts().json ?? false);
|
|
1402
|
-
});
|
|
1403
|
-
|
|
1404
|
-
const dmsCommand = program.command("dms").description("Direct messages");
|
|
1405
|
-
|
|
1406
|
-
const syncCommand = program
|
|
1407
|
-
.command("sync")
|
|
1408
|
-
.description("Refresh live Twitter collections into the local store");
|
|
1409
|
-
|
|
1410
|
-
syncCommand
|
|
1411
|
-
.command("timeline")
|
|
1412
|
-
.description("Refresh live home timeline through xurl or bird")
|
|
1413
|
-
.option("--account <accountId>", "Account id")
|
|
1414
|
-
.option("--mode <mode>", "auto, xurl, or bird", "auto")
|
|
1415
|
-
.option("--limit <n>", "Result limit", "100")
|
|
1416
|
-
.option("--max-pages <n>", "Stop after N xurl pages", "3")
|
|
1417
|
-
.option("--for-you", 'Fetch "For You" instead of chronological Following')
|
|
1418
|
-
.option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
|
|
1419
|
-
.option("--refresh", "Bypass live-cache freshness window")
|
|
1420
|
-
.action(async (options) => {
|
|
1421
|
-
const result = await syncHomeTimeline({
|
|
1422
|
-
account: options.account,
|
|
1423
|
-
mode: options.mode,
|
|
1424
|
-
limit: Number(options.limit),
|
|
1425
|
-
maxPages: Number(options.maxPages),
|
|
1426
|
-
following: !options.forYou,
|
|
1427
|
-
refresh: Boolean(options.refresh),
|
|
1428
|
-
cacheTtlMs: Number(options.cacheTtl) * 1000,
|
|
1429
|
-
});
|
|
1430
|
-
await autoSyncAfterWrite();
|
|
1431
|
-
print(result, true);
|
|
1432
|
-
});
|
|
1433
|
-
|
|
1434
|
-
syncCommand
|
|
1435
|
-
.command("mentions")
|
|
1436
|
-
.description("Refresh live mentions through xurl or bird")
|
|
1437
|
-
.option("--account <accountId>", "Account id")
|
|
1438
|
-
.option("--mode <mode>", "auto, bird, or xurl", "auto")
|
|
1439
|
-
.option("--limit <n>", "Result limit per page", "20")
|
|
1440
|
-
.option("--max-pages <n>", "Stop after N pages")
|
|
1441
|
-
.option("--since-id <id>", "Fetch mentions newer than this tweet id")
|
|
1442
|
-
.option("--start-time <iso>", "Fetch mentions created at or after this time")
|
|
1443
|
-
.option("--refresh", "Bypass live-cache freshness window")
|
|
1444
|
-
.option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
|
|
1445
|
-
.action(async (options) => {
|
|
1446
|
-
try {
|
|
1447
|
-
const result = await syncMentions({
|
|
1448
|
-
account: options.account,
|
|
1449
|
-
mode: options.mode,
|
|
1450
|
-
limit: Number(options.limit),
|
|
1451
|
-
maxPages: options.maxPages ? Number(options.maxPages) : undefined,
|
|
1452
|
-
sinceId: options.sinceId,
|
|
1453
|
-
startTime: options.startTime,
|
|
1454
|
-
refresh: Boolean(options.refresh),
|
|
1455
|
-
cacheTtlMs: Number(options.cacheTtl) * 1000,
|
|
1456
|
-
});
|
|
1457
|
-
await autoSyncAfterWrite();
|
|
1458
|
-
print(result, true);
|
|
1459
|
-
if (result.partial) {
|
|
1460
|
-
process.exitCode = 5;
|
|
1461
|
-
}
|
|
1462
|
-
} catch (error) {
|
|
1463
|
-
print(
|
|
1464
|
-
{
|
|
1465
|
-
ok: false,
|
|
1466
|
-
kind: "mentions",
|
|
1467
|
-
mode: options.mode ?? "xurl",
|
|
1468
|
-
error: errorMessage(error),
|
|
1469
|
-
},
|
|
1470
|
-
true,
|
|
1471
|
-
);
|
|
1472
|
-
process.exitCode = 1;
|
|
1473
|
-
}
|
|
1474
|
-
});
|
|
1475
|
-
|
|
1476
|
-
syncCommand
|
|
1477
|
-
.command("authored")
|
|
1478
|
-
.description("Refresh authenticated authored tweets through xurl")
|
|
1479
|
-
.option("--account <accountId>", "Account id")
|
|
1480
|
-
.option("--mode <mode>", "xurl", "xurl")
|
|
1481
|
-
.option("--limit <n>", "X API page size", "100")
|
|
1482
|
-
.option("--max-pages <n>", "Stop after N pages and resume later")
|
|
1483
|
-
.option("--since-id <tweetId>", "Override the stored since_id cursor")
|
|
1484
|
-
.option(
|
|
1485
|
-
"--until-id <tweetId>",
|
|
1486
|
-
"Fetch tweets older than this id without moving the cursor",
|
|
1487
|
-
)
|
|
1488
|
-
.action(async (options) => {
|
|
1489
|
-
try {
|
|
1490
|
-
const result = await syncAuthoredTweets({
|
|
1491
|
-
account: options.account,
|
|
1492
|
-
mode: options.mode as AuthoredSyncMode,
|
|
1493
|
-
limit: Number(options.limit),
|
|
1494
|
-
maxPages: options.maxPages ? Number(options.maxPages) : undefined,
|
|
1495
|
-
sinceId: options.sinceId,
|
|
1496
|
-
untilId: options.untilId,
|
|
1497
|
-
});
|
|
1498
|
-
await autoSyncAfterWrite();
|
|
1499
|
-
print(result, true);
|
|
1500
|
-
if (result.partial) {
|
|
1501
|
-
process.exitCode = 5;
|
|
1502
|
-
}
|
|
1503
|
-
} catch (error) {
|
|
1504
|
-
print(
|
|
1505
|
-
{
|
|
1506
|
-
ok: false,
|
|
1507
|
-
kind: "authored",
|
|
1508
|
-
source: "xurl",
|
|
1509
|
-
error: errorMessage(error),
|
|
1510
|
-
},
|
|
1511
|
-
true,
|
|
1512
|
-
);
|
|
1513
|
-
process.exitCode =
|
|
1514
|
-
error instanceof AuthoredSyncError ? error.exitCode : 1;
|
|
1515
|
-
}
|
|
1516
|
-
});
|
|
1517
|
-
|
|
1518
|
-
syncCommand
|
|
1519
|
-
.command("mention-threads")
|
|
1520
|
-
.description(
|
|
1521
|
-
"Fetch tweet conversation context for recent mentions through bird or xurl",
|
|
1522
|
-
)
|
|
1523
|
-
.option("--account <accountId>", "Account id")
|
|
1524
|
-
.option("--mode <mode>", "bird or xurl", "bird")
|
|
1525
|
-
.option("--limit <n>", "Recent mentions to inspect", "30")
|
|
1526
|
-
.option("--delay-ms <n>", "Delay between thread fetches", "1500")
|
|
1527
|
-
.option("--timeout-ms <n>", "Per-thread timeout", "15000")
|
|
1528
|
-
.option("--all", "Fetch all retrievable thread pages")
|
|
1529
|
-
.option("--max-pages <n>", "Stop after N pages")
|
|
1530
|
-
.action(async (options) => {
|
|
1531
|
-
try {
|
|
1532
|
-
const result = await syncMentionThreads({
|
|
1533
|
-
account: options.account,
|
|
1534
|
-
mode: options.mode,
|
|
1535
|
-
limit: Number(options.limit),
|
|
1536
|
-
delayMs: Number(options.delayMs),
|
|
1537
|
-
timeoutMs: Number(options.timeoutMs),
|
|
1538
|
-
all: Boolean(options.all),
|
|
1539
|
-
maxPages: options.maxPages ? Number(options.maxPages) : undefined,
|
|
1540
|
-
});
|
|
1541
|
-
await autoSyncAfterWrite();
|
|
1542
|
-
print(result, true);
|
|
1543
|
-
if (result.partial) {
|
|
1544
|
-
process.exitCode = 5;
|
|
1545
|
-
}
|
|
1546
|
-
} catch (error) {
|
|
1547
|
-
print(
|
|
1548
|
-
{
|
|
1549
|
-
ok: false,
|
|
1550
|
-
kind: "mention-threads",
|
|
1551
|
-
mode: options.mode ?? "bird",
|
|
1552
|
-
error: errorMessage(error),
|
|
1553
|
-
},
|
|
1554
|
-
true,
|
|
1555
|
-
);
|
|
1556
|
-
process.exitCode = 1;
|
|
1557
|
-
}
|
|
1558
|
-
});
|
|
1559
|
-
|
|
1560
|
-
for (const kind of ["likes", "bookmarks"] as const) {
|
|
1561
|
-
syncCommand
|
|
1562
|
-
.command(kind)
|
|
1563
|
-
.description(`Refresh live ${kind} through xurl or bird`)
|
|
1564
|
-
.option("--account <accountId>", "Account id")
|
|
1565
|
-
.option("--mode <mode>", "auto, xurl, or bird", "auto")
|
|
1566
|
-
.option("--limit <n>", "Per-page/result limit", "20")
|
|
1567
|
-
.option("--all", "Fetch every retrievable page")
|
|
1568
|
-
.option(
|
|
1569
|
-
"--max-pages <n>",
|
|
1570
|
-
"Stop after N pages when using --all or --early-stop",
|
|
1571
|
-
)
|
|
1572
|
-
.option("--early-stop", "Stop when a fetched page is already fully local")
|
|
1573
|
-
.option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
|
|
1574
|
-
.option("--refresh", "Bypass live-cache freshness window")
|
|
1575
|
-
.action(async (options) => {
|
|
1576
|
-
const result = await syncTimelineCollection({
|
|
1577
|
-
kind,
|
|
1578
|
-
account: options.account,
|
|
1579
|
-
mode: options.mode as TimelineCollectionMode,
|
|
1580
|
-
limit: Number(options.limit),
|
|
1581
|
-
all: Boolean(options.all) || options.maxPages !== undefined,
|
|
1582
|
-
maxPages: options.maxPages ? Number(options.maxPages) : undefined,
|
|
1583
|
-
refresh: Boolean(options.refresh),
|
|
1584
|
-
cacheTtlMs: Number(options.cacheTtl) * 1000,
|
|
1585
|
-
earlyStop: Boolean(options.earlyStop),
|
|
1586
|
-
});
|
|
1587
|
-
await autoSyncAfterWrite();
|
|
1588
|
-
print(result, true);
|
|
1589
|
-
});
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
|
-
for (const direction of ["followers", "following"] as const) {
|
|
1593
|
-
syncCommand
|
|
1594
|
-
.command(direction)
|
|
1595
|
-
.description(
|
|
1596
|
-
`Dry-run or refresh live ${direction} into the local follow graph`,
|
|
1597
|
-
)
|
|
1598
|
-
.option("--account <accountId>", "Account id")
|
|
1599
|
-
.option("--mode <mode>", "auto, bird, or xurl", "auto")
|
|
1600
|
-
.option("--limit <n>", "X API users per page", "1000")
|
|
1601
|
-
.option("--max-pages <n>", "Stop after N pages")
|
|
1602
|
-
.option("--max-resources <n>", "Stop after N unique users")
|
|
1603
|
-
.option("--cache-ttl <seconds>", "Live-cache freshness window", "86400")
|
|
1604
|
-
.option("--refresh", "Bypass the live-cache freshness window")
|
|
1605
|
-
.option("--allow-partial", "Acknowledge capped/incomplete snapshot")
|
|
1606
|
-
.option("--yes", "Confirm live sync or fresh-cache merge")
|
|
1607
|
-
.action(async (options) => {
|
|
1608
|
-
try {
|
|
1609
|
-
const result = await syncFollowGraph({
|
|
1610
|
-
direction,
|
|
1611
|
-
account: options.account,
|
|
1612
|
-
mode: options.mode,
|
|
1613
|
-
limit: Number(options.limit),
|
|
1614
|
-
maxPages: options.maxPages ? Number(options.maxPages) : undefined,
|
|
1615
|
-
maxResources: options.maxResources
|
|
1616
|
-
? Number(options.maxResources)
|
|
1617
|
-
: undefined,
|
|
1618
|
-
cacheTtlMs: Number(options.cacheTtl) * 1000,
|
|
1619
|
-
refresh: Boolean(options.refresh),
|
|
1620
|
-
allowPartial: Boolean(options.allowPartial),
|
|
1621
|
-
yes: Boolean(options.yes),
|
|
1622
|
-
});
|
|
1623
|
-
if (!result.dryRun) {
|
|
1624
|
-
await autoSyncAfterWrite();
|
|
1625
|
-
}
|
|
1626
|
-
print(result, true);
|
|
1627
|
-
} catch (error) {
|
|
1628
|
-
print({ ok: false, direction, error: errorMessage(error) }, true);
|
|
1629
|
-
process.exitCode = 1;
|
|
1630
|
-
}
|
|
1631
|
-
});
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
|
-
const jobsCommand = program
|
|
1635
|
-
.command("jobs")
|
|
1636
|
-
.description("Run and install background Birdclaw jobs");
|
|
1637
|
-
|
|
1638
|
-
jobsCommand
|
|
1639
|
-
.command("sync-account")
|
|
1640
|
-
.description("Refresh live account timelines and append a JSONL audit entry")
|
|
1641
|
-
.option("--account <accountId>", "Account id")
|
|
1642
|
-
.option(
|
|
1643
|
-
"--steps <steps>",
|
|
1644
|
-
"Comma list: timeline,mentions,mention-threads,likes,bookmarks,dms",
|
|
1645
|
-
)
|
|
1646
|
-
.option("--mode <mode>", "auto, xurl, or bird for likes/bookmarks", "auto")
|
|
1647
|
-
.option("--limit <n>", "Per-page/result limit", "100")
|
|
1648
|
-
.option("--max-pages <n>", "Stop after N pages", "3")
|
|
1649
|
-
.option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
|
|
1650
|
-
.option("--refresh", "Bypass live-cache freshness window")
|
|
1651
|
-
.option(
|
|
1652
|
-
"--allow-bird-account",
|
|
1653
|
-
"Assert bird cookies match --account for Bird-backed steps",
|
|
1654
|
-
)
|
|
1655
|
-
.option("--log <path>", "Audit JSONL path")
|
|
1656
|
-
.action(async (options) => {
|
|
1657
|
-
const result = await runAccountSyncJob({
|
|
1658
|
-
account: options.account,
|
|
1659
|
-
steps: parseAccountSyncSteps(options.steps),
|
|
1660
|
-
mode: options.mode as TimelineCollectionMode,
|
|
1661
|
-
limit: Number(options.limit),
|
|
1662
|
-
maxPages: Number(options.maxPages),
|
|
1663
|
-
refresh: Boolean(options.refresh),
|
|
1664
|
-
cacheTtlMs: Number(options.cacheTtl) * 1000,
|
|
1665
|
-
allowBirdAccount: Boolean(options.allowBirdAccount),
|
|
1666
|
-
logPath: options.log,
|
|
1667
|
-
});
|
|
1668
|
-
print(result, true);
|
|
1669
|
-
if (!result.ok) {
|
|
1670
|
-
process.exitCode = 1;
|
|
1671
|
-
}
|
|
1672
|
-
});
|
|
1673
|
-
|
|
1674
|
-
jobsCommand
|
|
1675
|
-
.command("install-account-launchd")
|
|
1676
|
-
.description("Install a LaunchAgent that runs account sync")
|
|
1677
|
-
.option("--label <label>", "LaunchAgent label")
|
|
1678
|
-
.option("--interval-seconds <seconds>", "Launch interval", "1800")
|
|
1679
|
-
.option("--program <path>", "birdclaw executable or command", "birdclaw")
|
|
1680
|
-
.option("--account <accountId>", "Account id")
|
|
1681
|
-
.option(
|
|
1682
|
-
"--steps <steps>",
|
|
1683
|
-
"Comma list: timeline,mentions,mention-threads,likes,bookmarks,dms",
|
|
1684
|
-
)
|
|
1685
|
-
.option("--mode <mode>", "auto, xurl, or bird for likes/bookmarks", "auto")
|
|
1686
|
-
.option("--limit <n>", "Per-page/result limit", "100")
|
|
1687
|
-
.option("--max-pages <n>", "Stop after N pages", "3")
|
|
1688
|
-
.option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
|
|
1689
|
-
.option("--no-refresh", "Allow live-cache reuse")
|
|
1690
|
-
.option(
|
|
1691
|
-
"--allow-bird-account",
|
|
1692
|
-
"Assert bird cookies match --account for Bird-backed steps",
|
|
1693
|
-
)
|
|
1694
|
-
.option("--log <path>", "Audit JSONL path")
|
|
1695
|
-
.option("--env-path <path>", "Shell env file to source before running")
|
|
1696
|
-
.option("--env-file <path>", "Deprecated alias for --env-path")
|
|
1697
|
-
.option("--stdout <path>", "launchd stdout path")
|
|
1698
|
-
.option("--stderr <path>", "launchd stderr path")
|
|
1699
|
-
.option("--launch-agents-dir <path>", "LaunchAgents directory")
|
|
1700
|
-
.option("--no-load", "Write plist without loading it")
|
|
1701
|
-
.action(async (options) => {
|
|
1702
|
-
const result = await installAccountSyncLaunchAgent({
|
|
1703
|
-
label: options.label,
|
|
1704
|
-
intervalSeconds: Number(options.intervalSeconds),
|
|
1705
|
-
program: options.program,
|
|
1706
|
-
account: options.account,
|
|
1707
|
-
steps: parseAccountSyncSteps(options.steps),
|
|
1708
|
-
mode: options.mode as TimelineCollectionMode,
|
|
1709
|
-
limit: Number(options.limit),
|
|
1710
|
-
maxPages: Number(options.maxPages),
|
|
1711
|
-
refresh: options.refresh,
|
|
1712
|
-
allowBirdAccount: Boolean(options.allowBirdAccount),
|
|
1713
|
-
cacheTtlSeconds: Number(options.cacheTtl),
|
|
1714
|
-
logPath: options.log,
|
|
1715
|
-
envFile: options.envPath ?? options.envFile,
|
|
1716
|
-
stdoutPath: options.stdout,
|
|
1717
|
-
stderrPath: options.stderr,
|
|
1718
|
-
launchAgentsDir: options.launchAgentsDir,
|
|
1719
|
-
load: options.load,
|
|
1720
|
-
});
|
|
1721
|
-
print(result, true);
|
|
1722
|
-
});
|
|
1723
|
-
|
|
1724
|
-
jobsCommand
|
|
1725
|
-
.command("sync-bookmarks")
|
|
1726
|
-
.description("Refresh live bookmarks and append a JSONL audit entry")
|
|
1727
|
-
.option("--account <accountId>", "Account id")
|
|
1728
|
-
.option("--mode <mode>", "auto, xurl, or bird", "auto")
|
|
1729
|
-
.option("--limit <n>", "Per-page/result limit", "100")
|
|
1730
|
-
.option("--all", "Fetch every retrievable page")
|
|
1731
|
-
.option("--max-pages <n>", "Stop after N pages", "5")
|
|
1732
|
-
.option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
|
|
1733
|
-
.option("--refresh", "Bypass live-cache freshness window")
|
|
1734
|
-
.option("--log <path>", "Audit JSONL path")
|
|
1735
|
-
.action(async (options) => {
|
|
1736
|
-
const result = await runBookmarkSyncJob({
|
|
1737
|
-
account: options.account,
|
|
1738
|
-
mode: options.mode as TimelineCollectionMode,
|
|
1739
|
-
limit: Number(options.limit),
|
|
1740
|
-
all: Boolean(options.all) || options.maxPages !== undefined,
|
|
1741
|
-
maxPages: options.all ? undefined : Number(options.maxPages),
|
|
1742
|
-
refresh: Boolean(options.refresh),
|
|
1743
|
-
cacheTtlMs: Number(options.cacheTtl) * 1000,
|
|
1744
|
-
logPath: options.log,
|
|
1745
|
-
});
|
|
1746
|
-
print(result, true);
|
|
1747
|
-
if (!result.ok) {
|
|
1748
|
-
process.exitCode = 1;
|
|
1749
|
-
}
|
|
1750
|
-
});
|
|
1751
|
-
|
|
1752
|
-
jobsCommand
|
|
1753
|
-
.command("install-bookmarks-launchd")
|
|
1754
|
-
.description("Install a LaunchAgent that runs bookmark sync every 3 hours")
|
|
1755
|
-
.option("--label <label>", "LaunchAgent label")
|
|
1756
|
-
.option("--interval-seconds <seconds>", "Launch interval", "10800")
|
|
1757
|
-
.option("--program <path>", "birdclaw executable or command", "birdclaw")
|
|
1758
|
-
.option("--mode <mode>", "auto, xurl, or bird", "auto")
|
|
1759
|
-
.option("--limit <n>", "Per-page/result limit", "100")
|
|
1760
|
-
.option("--all", "Fetch every retrievable page")
|
|
1761
|
-
.option("--max-pages <n>", "Stop after N pages", "5")
|
|
1762
|
-
.option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
|
|
1763
|
-
.option("--no-refresh", "Allow live-cache reuse")
|
|
1764
|
-
.option("--log <path>", "Audit JSONL path")
|
|
1765
|
-
.option("--env-path <path>", "Shell env file to source before running")
|
|
1766
|
-
.option("--env-file <path>", "Deprecated alias for --env-path")
|
|
1767
|
-
.option("--stdout <path>", "launchd stdout path")
|
|
1768
|
-
.option("--stderr <path>", "launchd stderr path")
|
|
1769
|
-
.option("--launch-agents-dir <path>", "LaunchAgents directory")
|
|
1770
|
-
.option("--no-load", "Write plist without loading it")
|
|
1771
|
-
.action(async (options) => {
|
|
1772
|
-
const result = await installBookmarkSyncLaunchAgent({
|
|
1773
|
-
label: options.label,
|
|
1774
|
-
intervalSeconds: Number(options.intervalSeconds),
|
|
1775
|
-
program: options.program,
|
|
1776
|
-
mode: options.mode as TimelineCollectionMode,
|
|
1777
|
-
limit: Number(options.limit),
|
|
1778
|
-
all: Boolean(options.all) || options.maxPages !== undefined,
|
|
1779
|
-
maxPages: options.all ? undefined : Number(options.maxPages),
|
|
1780
|
-
refresh: options.refresh,
|
|
1781
|
-
cacheTtlSeconds: Number(options.cacheTtl),
|
|
1782
|
-
logPath: options.log,
|
|
1783
|
-
envFile: options.envPath ?? options.envFile,
|
|
1784
|
-
stdoutPath: options.stdout,
|
|
1785
|
-
stderrPath: options.stderr,
|
|
1786
|
-
launchAgentsDir: options.launchAgentsDir,
|
|
1787
|
-
load: options.load,
|
|
1788
|
-
});
|
|
1789
|
-
print(result, true);
|
|
1790
|
-
});
|
|
1791
|
-
|
|
1792
|
-
dmsCommand
|
|
1793
|
-
.command("list")
|
|
1794
|
-
.option("--account <accountId>", "Account id")
|
|
1795
|
-
.option("--mode <mode>", "auto, bird, or xurl", "bird")
|
|
1796
|
-
.option("--refresh", "Refresh live DMs before listing")
|
|
1797
|
-
.option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
|
|
1798
|
-
.option("--inbox <kind>", "all, accepted, or requests", "all")
|
|
1799
|
-
.option("--max-pages <n>", "Additional accepted/request pages to sync", "0")
|
|
1800
|
-
.option("--all-pages", "Fetch all accepted/request pages while syncing")
|
|
1801
|
-
.option("--page-delay-ms <n>", "Delay between live DM page requests", "0")
|
|
1802
|
-
.option("--participant <value>")
|
|
1803
|
-
.option("--min-followers <n>", "Minimum sender follower count")
|
|
1804
|
-
.option("--max-followers <n>", "Maximum sender follower count")
|
|
1805
|
-
.option("--min-influence-score <n>", "Minimum derived influence score")
|
|
1806
|
-
.option("--max-influence-score <n>", "Maximum derived influence score")
|
|
1807
|
-
.option("--sort <mode>", "recent or followers", "recent")
|
|
1808
|
-
.option(
|
|
1809
|
-
"--resolve-profiles",
|
|
1810
|
-
"Resolve placeholder DM profiles through cache/bird/xurl",
|
|
1811
|
-
)
|
|
1812
|
-
.option("--expand-urls", "Expand URLs through the persistent URL cache")
|
|
1813
|
-
.option("--refresh-profile-cache", "Bypass profile lookup cache")
|
|
1814
|
-
.option("--refresh-url-cache", "Bypass URL expansion cache")
|
|
1815
|
-
.option(
|
|
1816
|
-
"--no-xurl-fallback",
|
|
1817
|
-
"Do not fall back to xurl after bird profile lookup",
|
|
1818
|
-
)
|
|
1819
|
-
.option("--replied", "Only replied threads")
|
|
1820
|
-
.option("--unreplied", "Only unreplied threads")
|
|
1821
|
-
.option("--limit <n>", "Limit results", "20")
|
|
1822
|
-
.action(async (options) => {
|
|
1823
|
-
const replyFilter = options.replied
|
|
1824
|
-
? "replied"
|
|
1825
|
-
: options.unreplied
|
|
1826
|
-
? "unreplied"
|
|
1827
|
-
: "all";
|
|
1828
|
-
const inbox = parseDmInboxOption(options.inbox);
|
|
1829
|
-
const mode = parseDmSyncModeOption(options.mode);
|
|
1830
|
-
const maxPages = parseNonNegativeIntegerOption(
|
|
1831
|
-
options.maxPages,
|
|
1832
|
-
"--max-pages",
|
|
1833
|
-
);
|
|
1834
|
-
const pageDelayMs = parseNonNegativeIntegerOption(
|
|
1835
|
-
options.pageDelayMs,
|
|
1836
|
-
"--page-delay-ms",
|
|
1837
|
-
);
|
|
1838
|
-
if (
|
|
1839
|
-
inbox === undefined ||
|
|
1840
|
-
mode === undefined ||
|
|
1841
|
-
maxPages === undefined ||
|
|
1842
|
-
pageDelayMs === undefined
|
|
1843
|
-
) {
|
|
1844
|
-
return;
|
|
1845
|
-
}
|
|
1846
|
-
if (options.refresh) {
|
|
1847
|
-
await syncDirectMessagesViaCachedBird({
|
|
1848
|
-
account: options.account,
|
|
1849
|
-
mode,
|
|
1850
|
-
limit: Number(options.limit),
|
|
1851
|
-
...(inbox !== "all" ? { inbox } : {}),
|
|
1852
|
-
...(maxPages > 0 ? { maxPages } : {}),
|
|
1853
|
-
...(options.allPages ? { allPages: true } : {}),
|
|
1854
|
-
...(pageDelayMs > 0 ? { pageDelayMs } : {}),
|
|
1855
|
-
refresh: true,
|
|
1856
|
-
cacheTtlMs: Number(options.cacheTtl) * 1000,
|
|
1857
|
-
});
|
|
1858
|
-
await autoSyncAfterWrite();
|
|
1859
|
-
} else {
|
|
1860
|
-
await autoUpdateBeforeRead();
|
|
1861
|
-
}
|
|
1862
|
-
const items = await enrichDmItems(
|
|
1863
|
-
{
|
|
1864
|
-
account: options.account,
|
|
1865
|
-
...(inbox !== "all" ? { inbox } : {}),
|
|
1866
|
-
participant: options.participant,
|
|
1867
|
-
minFollowers: options.minFollowers
|
|
1868
|
-
? Number(options.minFollowers)
|
|
1869
|
-
: undefined,
|
|
1870
|
-
maxFollowers: options.maxFollowers
|
|
1871
|
-
? Number(options.maxFollowers)
|
|
1872
|
-
: undefined,
|
|
1873
|
-
minInfluenceScore: options.minInfluenceScore
|
|
1874
|
-
? Number(options.minInfluenceScore)
|
|
1875
|
-
: undefined,
|
|
1876
|
-
maxInfluenceScore: options.maxInfluenceScore
|
|
1877
|
-
? Number(options.maxInfluenceScore)
|
|
1878
|
-
: undefined,
|
|
1879
|
-
sort:
|
|
1880
|
-
options.sort === "followers" || options.sort === "influence"
|
|
1881
|
-
? "followers"
|
|
1882
|
-
: "recent",
|
|
1883
|
-
replyFilter,
|
|
1884
|
-
limit: Number(options.limit),
|
|
1885
|
-
},
|
|
1886
|
-
{
|
|
1887
|
-
resolveProfiles: Boolean(options.resolveProfiles),
|
|
1888
|
-
expandUrls: Boolean(options.expandUrls),
|
|
1889
|
-
refreshProfileCache: Boolean(options.refreshProfileCache),
|
|
1890
|
-
refreshUrlCache: Boolean(options.refreshUrlCache),
|
|
1891
|
-
xurlFallback: options.xurlFallback,
|
|
1892
|
-
},
|
|
1893
|
-
);
|
|
1894
|
-
print(items, program.opts().json ?? false);
|
|
1895
|
-
});
|
|
1896
|
-
|
|
1897
|
-
dmsCommand
|
|
1898
|
-
.command("sync")
|
|
1899
|
-
.description("Refresh live direct messages into the local store")
|
|
1900
|
-
.option("--account <accountId>", "Account id")
|
|
1901
|
-
.option("--mode <mode>", "auto, bird, or xurl", "bird")
|
|
1902
|
-
.option("--limit <n>", "Limit messages", "20")
|
|
1903
|
-
.option("--inbox <kind>", "all, accepted, or requests", "all")
|
|
1904
|
-
.option("--max-pages <n>", "Additional accepted/request pages to sync", "0")
|
|
1905
|
-
.option("--all-pages", "Fetch all accepted/request pages")
|
|
1906
|
-
.option("--page-delay-ms <n>", "Delay between live DM page requests", "0")
|
|
1907
|
-
.option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
|
|
1908
|
-
.option("--refresh", "Bypass live-cache freshness window")
|
|
1909
|
-
.action(async (options) => {
|
|
1910
|
-
const inbox = parseDmInboxOption(options.inbox);
|
|
1911
|
-
const mode = parseDmSyncModeOption(options.mode);
|
|
1912
|
-
const maxPages = parseNonNegativeIntegerOption(
|
|
1913
|
-
options.maxPages,
|
|
1914
|
-
"--max-pages",
|
|
1915
|
-
);
|
|
1916
|
-
const pageDelayMs = parseNonNegativeIntegerOption(
|
|
1917
|
-
options.pageDelayMs,
|
|
1918
|
-
"--page-delay-ms",
|
|
1919
|
-
);
|
|
1920
|
-
if (
|
|
1921
|
-
inbox === undefined ||
|
|
1922
|
-
mode === undefined ||
|
|
1923
|
-
maxPages === undefined ||
|
|
1924
|
-
pageDelayMs === undefined
|
|
1925
|
-
) {
|
|
1926
|
-
return;
|
|
1927
|
-
}
|
|
1928
|
-
const result = await syncDirectMessagesViaCachedBird({
|
|
1929
|
-
account: options.account,
|
|
1930
|
-
mode,
|
|
1931
|
-
limit: Number(options.limit),
|
|
1932
|
-
...(inbox !== "all" ? { inbox } : {}),
|
|
1933
|
-
...(maxPages > 0 ? { maxPages } : {}),
|
|
1934
|
-
...(options.allPages ? { allPages: true } : {}),
|
|
1935
|
-
...(pageDelayMs > 0 ? { pageDelayMs } : {}),
|
|
1936
|
-
refresh: Boolean(options.refresh),
|
|
1937
|
-
cacheTtlMs: Number(options.cacheTtl) * 1000,
|
|
1938
|
-
});
|
|
1939
|
-
await autoSyncAfterWrite();
|
|
1940
|
-
print(result, true);
|
|
1941
|
-
});
|
|
1942
|
-
|
|
1943
|
-
for (const action of ["accept", "reject", "block"] as const) {
|
|
1944
|
-
const command = dmsCommand
|
|
1945
|
-
.command(`${action} <conversationId>`)
|
|
1946
|
-
.description(`${action} a live DM message request through bird`);
|
|
1947
|
-
if (action === "block") {
|
|
1948
|
-
command
|
|
1949
|
-
.option("--max-pages <n>", "Additional timeline pages to search", "3")
|
|
1950
|
-
.option("--all-pages", "Search all accepted/request timeline pages");
|
|
1951
|
-
}
|
|
1952
|
-
command.action(async (conversationId, options) => {
|
|
1953
|
-
const maxPages =
|
|
1954
|
-
action === "block"
|
|
1955
|
-
? parseNonNegativeIntegerOption(options.maxPages, "--max-pages")
|
|
1956
|
-
: undefined;
|
|
1957
|
-
if (action === "block" && maxPages === undefined) {
|
|
1958
|
-
return;
|
|
1959
|
-
}
|
|
1960
|
-
const result = await runDirectMessageRequestMutationViaBird({
|
|
1961
|
-
action,
|
|
1962
|
-
conversationId,
|
|
1963
|
-
...(action === "block" && maxPages !== undefined ? { maxPages } : {}),
|
|
1964
|
-
...(action === "block" && options.allPages ? { allPages: true } : {}),
|
|
1965
|
-
});
|
|
1966
|
-
if (result.success) {
|
|
1967
|
-
applyDmRequestMutationToLocalStore(conversationId, action);
|
|
1968
|
-
} else {
|
|
1969
|
-
process.exitCode = 1;
|
|
1970
|
-
}
|
|
1971
|
-
await autoSyncAfterWrite();
|
|
1972
|
-
print(result, true);
|
|
1973
|
-
});
|
|
1974
|
-
}
|
|
1975
|
-
|
|
1976
|
-
registerModerationCommands({
|
|
1977
|
-
program,
|
|
1978
|
-
print,
|
|
1979
|
-
asJson: () => program.opts().json ?? false,
|
|
1980
|
-
importBlocklist,
|
|
1981
|
-
resolveActionOptions,
|
|
1982
|
-
});
|
|
1983
|
-
|
|
1984
|
-
const composeCommand = program
|
|
1985
|
-
.command("compose")
|
|
1986
|
-
.description("Create local/xurl actions");
|
|
1987
|
-
|
|
1988
|
-
composeCommand
|
|
1989
|
-
.command("post <text>")
|
|
1990
|
-
.option("--account <accountId>", "Account id", "acct_primary")
|
|
1991
|
-
.action(async (text, options) => {
|
|
1992
|
-
const result = await createPost(options.account, text);
|
|
1993
|
-
await autoSyncAfterWrite();
|
|
1994
|
-
print(result, program.opts().json ?? false);
|
|
1995
|
-
});
|
|
1996
|
-
|
|
1997
|
-
composeCommand
|
|
1998
|
-
.command("reply <tweetId> <text>")
|
|
1999
|
-
.option("--account <accountId>", "Account id", "acct_primary")
|
|
2000
|
-
.action(async (tweetId, text, options) => {
|
|
2001
|
-
const result = await createTweetReply(options.account, tweetId, text);
|
|
2002
|
-
await autoSyncAfterWrite();
|
|
2003
|
-
print(result, program.opts().json ?? false);
|
|
2004
|
-
});
|
|
2005
|
-
|
|
2006
|
-
composeCommand
|
|
2007
|
-
.command("dm <conversationId> <text>")
|
|
2008
|
-
.description("Reply inside an existing DM conversation")
|
|
2009
|
-
.action(async (conversationId, text) => {
|
|
2010
|
-
const result = await createDmReply(conversationId, text);
|
|
2011
|
-
await autoSyncAfterWrite();
|
|
2012
|
-
print(result, program.opts().json ?? false);
|
|
2013
|
-
});
|
|
2014
|
-
|
|
2015
|
-
program
|
|
2016
|
-
.command("inbox")
|
|
2017
|
-
.option("--kind <kind>", "mixed, mentions, or dms", "mixed")
|
|
2018
|
-
.option("--min-score <n>", "Minimum rank", "0")
|
|
2019
|
-
.option("--hide-low-signal", "Hide low-signal items")
|
|
2020
|
-
.option("--score", "Score top items with OpenAI before listing")
|
|
2021
|
-
.option("--limit <n>", "Limit results", "20")
|
|
2022
|
-
.action(async (options) => {
|
|
2023
|
-
await autoUpdateBeforeRead();
|
|
2024
|
-
const kind =
|
|
2025
|
-
options.kind === "mentions" || options.kind === "dms"
|
|
2026
|
-
? options.kind
|
|
2027
|
-
: "mixed";
|
|
2028
|
-
if (options.score) {
|
|
2029
|
-
await scoreInbox({
|
|
2030
|
-
kind,
|
|
2031
|
-
limit: Number(options.limit),
|
|
2032
|
-
});
|
|
2033
|
-
await autoSyncAfterWrite();
|
|
2034
|
-
}
|
|
2035
|
-
print(
|
|
2036
|
-
listInboxItems({
|
|
2037
|
-
kind,
|
|
2038
|
-
minScore: Number(options.minScore),
|
|
2039
|
-
hideLowSignal: Boolean(options.hideLowSignal),
|
|
2040
|
-
limit: Number(options.limit),
|
|
2041
|
-
}),
|
|
2042
|
-
program.opts().json ?? false,
|
|
2043
|
-
);
|
|
2044
|
-
});
|
|
2045
|
-
|
|
2046
|
-
const graphCommand = program
|
|
2047
|
-
.command("graph")
|
|
2048
|
-
.description("Query the local cache-only follow graph");
|
|
2049
|
-
|
|
2050
|
-
graphCommand
|
|
2051
|
-
.command("summary")
|
|
2052
|
-
.description("Summarize cached followers, following, mutuals, and snapshots")
|
|
2053
|
-
.option("--account <accountId>", "Account id")
|
|
2054
|
-
.action(async (options) => {
|
|
2055
|
-
await autoUpdateBeforeRead();
|
|
2056
|
-
print(getFollowGraphSummary({ account: options.account }), true);
|
|
2057
|
-
});
|
|
2058
|
-
|
|
2059
|
-
graphCommand
|
|
2060
|
-
.command("top-followers")
|
|
2061
|
-
.description("List current followers sorted by their follower count")
|
|
2062
|
-
.option("--account <accountId>", "Account id")
|
|
2063
|
-
.option("--limit <n>", "Limit results", "20")
|
|
2064
|
-
.action(async (options) => {
|
|
2065
|
-
await autoUpdateBeforeRead();
|
|
2066
|
-
print(
|
|
2067
|
-
listTopFollowers({
|
|
2068
|
-
account: options.account,
|
|
2069
|
-
limit: Number(options.limit),
|
|
2070
|
-
}),
|
|
2071
|
-
true,
|
|
2072
|
-
);
|
|
2073
|
-
});
|
|
2074
|
-
|
|
2075
|
-
graphCommand
|
|
2076
|
-
.command("unfollowed")
|
|
2077
|
-
.description("List cached ended follow edges since a date")
|
|
2078
|
-
.requiredOption("--date <date>", "YYYY-MM-DD or ISO timestamp")
|
|
2079
|
-
.option("--account <accountId>", "Account id")
|
|
2080
|
-
.option("--direction <direction>", "followers or following", "followers")
|
|
2081
|
-
.option("--limit <n>", "Limit results", "100")
|
|
2082
|
-
.action(async (options) => {
|
|
2083
|
-
await autoUpdateBeforeRead();
|
|
2084
|
-
print(
|
|
2085
|
-
listUnfollowedSince({
|
|
2086
|
-
account: options.account,
|
|
2087
|
-
date: options.date,
|
|
2088
|
-
direction:
|
|
2089
|
-
options.direction === "following" ? "following" : "followers",
|
|
2090
|
-
limit: Number(options.limit),
|
|
2091
|
-
}),
|
|
2092
|
-
true,
|
|
2093
|
-
);
|
|
2094
|
-
});
|
|
2095
|
-
|
|
2096
|
-
graphCommand
|
|
2097
|
-
.command("events")
|
|
2098
|
-
.description("List cached append-only follow graph events")
|
|
2099
|
-
.option("--account <accountId>", "Account id")
|
|
2100
|
-
.option("--direction <direction>", "followers or following")
|
|
2101
|
-
.option("--kind <kind>", "started or ended")
|
|
2102
|
-
.option("--since <date>", "YYYY-MM-DD or ISO timestamp")
|
|
2103
|
-
.option("--until <date>", "YYYY-MM-DD or ISO timestamp")
|
|
2104
|
-
.option("--limit <n>", "Limit results", "100")
|
|
2105
|
-
.action(async (options) => {
|
|
2106
|
-
await autoUpdateBeforeRead();
|
|
2107
|
-
print(
|
|
2108
|
-
listFollowEvents({
|
|
2109
|
-
account: options.account,
|
|
2110
|
-
direction:
|
|
2111
|
-
options.direction === "followers" || options.direction === "following"
|
|
2112
|
-
? options.direction
|
|
2113
|
-
: undefined,
|
|
2114
|
-
kind:
|
|
2115
|
-
options.kind === "started" || options.kind === "ended"
|
|
2116
|
-
? options.kind
|
|
2117
|
-
: undefined,
|
|
2118
|
-
since: options.since,
|
|
2119
|
-
until: options.until,
|
|
2120
|
-
limit: Number(options.limit),
|
|
2121
|
-
}),
|
|
2122
|
-
true,
|
|
2123
|
-
);
|
|
2124
|
-
});
|
|
2125
|
-
|
|
2126
|
-
graphCommand
|
|
2127
|
-
.command("non-mutual-following")
|
|
2128
|
-
.description("List current following who are not current followers")
|
|
2129
|
-
.option("--account <accountId>", "Account id")
|
|
2130
|
-
.option("--sort <mode>", "followers or handle", "followers")
|
|
2131
|
-
.option("--limit <n>", "Limit results", "100")
|
|
2132
|
-
.action(async (options) => {
|
|
2133
|
-
await autoUpdateBeforeRead();
|
|
2134
|
-
print(
|
|
2135
|
-
listNonMutualFollowing({
|
|
2136
|
-
account: options.account,
|
|
2137
|
-
sort: options.sort === "handle" ? "handle" : "followers",
|
|
2138
|
-
limit: Number(options.limit),
|
|
2139
|
-
}),
|
|
2140
|
-
true,
|
|
2141
|
-
);
|
|
2142
|
-
});
|
|
2143
|
-
|
|
2144
|
-
graphCommand
|
|
2145
|
-
.command("mutuals")
|
|
2146
|
-
.description("List profiles that are both followers and following")
|
|
2147
|
-
.option("--account <accountId>", "Account id")
|
|
2148
|
-
.option("--limit <n>", "Limit results", "100")
|
|
2149
|
-
.action(async (options) => {
|
|
2150
|
-
await autoUpdateBeforeRead();
|
|
2151
|
-
print(
|
|
2152
|
-
listMutuals({
|
|
2153
|
-
account: options.account,
|
|
2154
|
-
limit: Number(options.limit),
|
|
2155
|
-
}),
|
|
2156
|
-
true,
|
|
2157
|
-
);
|
|
2158
|
-
});
|
|
2159
|
-
|
|
2160
|
-
program
|
|
2161
|
-
.command("db stats")
|
|
2162
|
-
.description("Show local storage and dataset stats")
|
|
2163
|
-
.action(async () => {
|
|
2164
|
-
await autoUpdateBeforeRead();
|
|
2165
|
-
const meta = await getQueryEnvelope();
|
|
2166
|
-
const paths = getBirdclawPaths();
|
|
2167
|
-
print(
|
|
2168
|
-
{
|
|
2169
|
-
paths,
|
|
2170
|
-
stats: meta.stats,
|
|
2171
|
-
transport: meta.transport,
|
|
2172
|
-
},
|
|
2173
|
-
program.opts().json ?? false,
|
|
2174
|
-
);
|
|
2175
|
-
});
|
|
2176
|
-
|
|
2177
|
-
const backupCommand = program
|
|
2178
|
-
.command("backup")
|
|
2179
|
-
.description("Export, import, and validate Git-friendly text backups");
|
|
2180
|
-
|
|
2181
|
-
backupCommand
|
|
2182
|
-
.command("export")
|
|
2183
|
-
.description("Export canonical JSONL backup shards")
|
|
2184
|
-
.requiredOption("--repo <path>", "Backup repository/path")
|
|
2185
|
-
.option("--commit", "Create a git commit in the backup repo")
|
|
2186
|
-
.option("--push", "Push the backup repo after committing")
|
|
2187
|
-
.option(
|
|
2188
|
-
"--message <message>",
|
|
2189
|
-
"Git commit message",
|
|
2190
|
-
"archive: update birdclaw backup",
|
|
2191
|
-
)
|
|
2192
|
-
.option("--no-validate", "Skip post-export validation")
|
|
2193
|
-
.action(async (options) => {
|
|
2194
|
-
const result = await exportBackup({
|
|
2195
|
-
repoPath: options.repo,
|
|
2196
|
-
commit: Boolean(options.commit) || Boolean(options.push),
|
|
2197
|
-
push: Boolean(options.push),
|
|
2198
|
-
message: options.message,
|
|
2199
|
-
validate: options.validate,
|
|
2200
|
-
});
|
|
2201
|
-
print(result, true);
|
|
2202
|
-
});
|
|
2203
|
-
|
|
2204
|
-
backupCommand
|
|
2205
|
-
.command("import <repo>")
|
|
2206
|
-
.description("Merge a canonical JSONL backup into the local SQLite store")
|
|
2207
|
-
.option("--no-validate", "Skip backup validation before import")
|
|
2208
|
-
.option("--replace", "Replace local portable tables instead of merging")
|
|
2209
|
-
.action(async (repo, options) => {
|
|
2210
|
-
const result = await importBackup({
|
|
2211
|
-
repoPath: repo,
|
|
2212
|
-
validate: options.validate,
|
|
2213
|
-
mode: options.replace ? "replace" : "merge",
|
|
2214
|
-
});
|
|
2215
|
-
print(result, true);
|
|
2216
|
-
});
|
|
2217
|
-
|
|
2218
|
-
backupCommand
|
|
2219
|
-
.command("sync")
|
|
2220
|
-
.description("Pull, merge-import, export, commit, and push a backup repo")
|
|
2221
|
-
.requiredOption("--repo <path>", "Backup repository/path")
|
|
2222
|
-
.option("--remote <url>", "Git remote to clone/configure")
|
|
2223
|
-
.option(
|
|
2224
|
-
"--message <message>",
|
|
2225
|
-
"Git commit message",
|
|
2226
|
-
"archive: sync birdclaw backup",
|
|
2227
|
-
)
|
|
2228
|
-
.action(async (options) => {
|
|
2229
|
-
const result = await syncBackup({
|
|
2230
|
-
repoPath: options.repo,
|
|
2231
|
-
remote: options.remote,
|
|
2232
|
-
message: options.message,
|
|
2233
|
-
});
|
|
2234
|
-
print(result, true);
|
|
2235
|
-
});
|
|
2236
|
-
|
|
2237
|
-
backupCommand
|
|
2238
|
-
.command("validate <repo>")
|
|
2239
|
-
.description("Validate backup manifest, shard hashes, and JSONL rows")
|
|
2240
|
-
.action(async (repo) => {
|
|
2241
|
-
const result = await validateBackup(repo);
|
|
2242
|
-
print(result, true);
|
|
2243
|
-
if (!result.ok) {
|
|
2244
|
-
process.exitCode = 1;
|
|
2245
|
-
}
|
|
2246
|
-
});
|
|
2247
|
-
|
|
2248
|
-
program
|
|
2249
|
-
.command("serve")
|
|
2250
|
-
.description("Run the local web app")
|
|
2251
|
-
.action(async () => {
|
|
2252
|
-
await autoUpdateBeforeRead();
|
|
2253
|
-
const child = spawn(
|
|
2254
|
-
process.execPath,
|
|
2255
|
-
[
|
|
2256
|
-
"node_modules/vite/bin/vite.js",
|
|
2257
|
-
"dev",
|
|
2258
|
-
"--host",
|
|
2259
|
-
"127.0.0.1",
|
|
2260
|
-
"--port",
|
|
2261
|
-
"3000",
|
|
2262
|
-
],
|
|
2263
|
-
{
|
|
2264
|
-
cwd: packageRoot,
|
|
2265
|
-
env: { ...process.env, BIRDCLAW_LOCAL_WEB: "1" },
|
|
2266
|
-
stdio: "inherit",
|
|
2267
|
-
detached: process.platform !== "win32",
|
|
2268
|
-
},
|
|
2269
|
-
);
|
|
2270
|
-
const forwardedSignals = [
|
|
2271
|
-
"SIGINT",
|
|
2272
|
-
"SIGTERM",
|
|
2273
|
-
"SIGHUP",
|
|
2274
|
-
"SIGQUIT",
|
|
2275
|
-
] as const;
|
|
2276
|
-
const forwardSignal = (signal: NodeJS.Signals) => {
|
|
2277
|
-
if (child.exitCode === null && child.signalCode === null) {
|
|
2278
|
-
signalChild(signal);
|
|
2279
|
-
}
|
|
2280
|
-
};
|
|
2281
|
-
const signalChild = (signal: NodeJS.Signals) => {
|
|
2282
|
-
if (child.pid === undefined) {
|
|
2283
|
-
return;
|
|
2284
|
-
}
|
|
2285
|
-
const targetPid = process.platform === "win32" ? child.pid : -child.pid;
|
|
2286
|
-
try {
|
|
2287
|
-
process.kill(targetPid, signal);
|
|
2288
|
-
} catch (error) {
|
|
2289
|
-
if (
|
|
2290
|
-
!(
|
|
2291
|
-
typeof error === "object" &&
|
|
2292
|
-
error !== null &&
|
|
2293
|
-
"code" in error &&
|
|
2294
|
-
error.code === "ESRCH"
|
|
2295
|
-
)
|
|
2296
|
-
) {
|
|
2297
|
-
throw error;
|
|
2298
|
-
}
|
|
2299
|
-
}
|
|
2300
|
-
};
|
|
2301
|
-
const removeSignalHandlers = () => {
|
|
2302
|
-
for (const signal of forwardedSignals) {
|
|
2303
|
-
process.removeListener(signal, forwardSignal);
|
|
2304
|
-
}
|
|
2305
|
-
};
|
|
2306
|
-
for (const signal of forwardedSignals) {
|
|
2307
|
-
process.on(signal, forwardSignal);
|
|
2308
|
-
}
|
|
2309
|
-
child.on("exit", (code, signal) => {
|
|
2310
|
-
removeSignalHandlers();
|
|
2311
|
-
if (signal) {
|
|
2312
|
-
process.kill(process.pid, signal);
|
|
2313
|
-
return;
|
|
2314
|
-
}
|
|
2315
|
-
process.exit(code ?? 0);
|
|
2316
|
-
});
|
|
2317
|
-
});
|
|
2318
|
-
|
|
2319
|
-
export async function runCli(argv = process.argv) {
|
|
2320
|
-
try {
|
|
2321
|
-
await program.parseAsync(argv);
|
|
2322
|
-
} finally {
|
|
2323
|
-
await closeDatabase();
|
|
2324
|
-
}
|
|
2325
|
-
}
|
|
2326
|
-
|
|
2327
|
-
/* v8 ignore next 5 */
|
|
2328
|
-
if (process.argv[1]) {
|
|
2329
|
-
const entryUrl = pathToFileURL(process.argv[1]).href;
|
|
2330
|
-
if (import.meta.url === entryUrl) {
|
|
2331
|
-
void runCli().catch((error) => {
|
|
2332
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
2333
|
-
process.exitCode = 1;
|
|
2334
|
-
});
|
|
2335
|
-
}
|
|
2336
|
-
}
|