birdclaw 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +92 -1
- package/README.md +75 -5
- package/package.json +8 -2
- package/scripts/browser-perf.mjs +1 -0
- package/scripts/start-test-server.mjs +16 -3
- package/src/cli.ts +812 -37
- package/src/components/AccountSwitcher.tsx +186 -0
- package/src/components/AppNav.tsx +37 -7
- package/src/components/AvatarChip.tsx +9 -3
- package/src/components/DmWorkspace.tsx +18 -8
- package/src/components/LinkPreviewCard.tsx +40 -18
- package/src/components/MarkdownViewer.tsx +818 -0
- package/src/components/ProfileAnalysisStream.tsx +428 -0
- package/src/components/ProfilePreview.tsx +120 -9
- package/src/components/SavedTimelineView.tsx +30 -8
- package/src/components/SyncNowButton.tsx +60 -25
- package/src/components/ThemeSlider.tsx +55 -50
- package/src/components/TimelineCard.tsx +240 -92
- package/src/components/TimelineRouteFrame.tsx +38 -8
- package/src/components/TweetMediaGrid.tsx +87 -38
- package/src/components/TweetRichText.tsx +45 -17
- package/src/components/account-selection.ts +64 -0
- package/src/components/useTimelineRouteData.ts +97 -13
- package/src/lib/account-sync-job.ts +666 -0
- package/src/lib/actions-transport.ts +216 -146
- package/src/lib/api-client.ts +128 -53
- package/src/lib/archive-finder.ts +78 -63
- package/src/lib/archive-import.ts +1593 -1291
- package/src/lib/authored-live.ts +262 -204
- package/src/lib/avatar-cache.ts +208 -43
- package/src/lib/backup.ts +1536 -954
- package/src/lib/bird-actions.ts +139 -57
- package/src/lib/bird-command.ts +101 -28
- package/src/lib/bird.ts +582 -194
- package/src/lib/blocklist.ts +40 -23
- package/src/lib/blocks-write.ts +129 -80
- package/src/lib/blocks.ts +165 -97
- package/src/lib/bookmark-sync-job.ts +250 -160
- package/src/lib/config.ts +35 -2
- package/src/lib/conversation-surface.ts +79 -48
- package/src/lib/data-sources.ts +219 -0
- package/src/lib/db.ts +95 -4
- package/src/lib/dms-live.ts +720 -66
- package/src/lib/effect-runtime.ts +45 -0
- package/src/lib/follow-graph.ts +224 -180
- package/src/lib/geocoding.ts +296 -0
- package/src/lib/http-effect.ts +222 -0
- package/src/lib/inbox.ts +74 -43
- package/src/lib/link-index.ts +88 -76
- package/src/lib/link-insights.ts +24 -0
- package/src/lib/link-preview-metadata.ts +472 -52
- package/src/lib/location.ts +137 -0
- package/src/lib/media-fetch.ts +286 -213
- package/src/lib/mention-threads-live.ts +445 -288
- package/src/lib/mentions-live.ts +549 -354
- package/src/lib/moderation-target.ts +102 -65
- package/src/lib/moderation-write.ts +77 -18
- package/src/lib/mutes-write.ts +129 -80
- package/src/lib/mutes.ts +8 -1
- package/src/lib/network-map.ts +382 -0
- package/src/lib/openai.ts +84 -53
- package/src/lib/period-digest.ts +1317 -0
- package/src/lib/profile-affiliation-hydration.ts +93 -54
- package/src/lib/profile-analysis.ts +1272 -0
- package/src/lib/profile-bio-entities.ts +1 -1
- package/src/lib/profile-hydration.ts +124 -72
- package/src/lib/profile-replies.ts +60 -43
- package/src/lib/profile-resolver.ts +402 -294
- package/src/lib/queries.ts +983 -203
- package/src/lib/research.ts +165 -120
- package/src/lib/search-discussion.ts +1016 -0
- package/src/lib/sqlite.ts +1 -0
- package/src/lib/timeline-collections-live.ts +204 -167
- package/src/lib/timeline-live.ts +325 -51
- package/src/lib/tweet-account-edges.ts +2 -0
- package/src/lib/tweet-lookup.ts +30 -19
- package/src/lib/tweet-render.ts +141 -1
- package/src/lib/tweet-search-live.ts +565 -0
- package/src/lib/types.ts +75 -3
- package/src/lib/ui.ts +31 -8
- package/src/lib/url-expansion.ts +226 -55
- package/src/lib/url-safety.ts +220 -0
- package/src/lib/web-sync.ts +222 -149
- package/src/lib/whois.ts +166 -147
- package/src/lib/x-web.ts +102 -71
- package/src/lib/xurl-rate-limits.ts +267 -0
- package/src/lib/xurl.ts +1185 -405
- package/src/routeTree.gen.ts +273 -0
- package/src/routes/__root.tsx +24 -5
- package/src/routes/api/action.tsx +127 -78
- package/src/routes/api/avatar.tsx +39 -30
- package/src/routes/api/blocks.tsx +26 -23
- package/src/routes/api/conversation.tsx +25 -14
- package/src/routes/api/data-sources.tsx +24 -0
- package/src/routes/api/inbox.tsx +27 -21
- package/src/routes/api/link-insights.tsx +31 -25
- package/src/routes/api/link-preview.tsx +25 -21
- package/src/routes/api/network-map.tsx +55 -0
- package/src/routes/api/period-digest.tsx +133 -0
- package/src/routes/api/profile-analysis.tsx +152 -0
- package/src/routes/api/profile-hydrate.tsx +31 -28
- package/src/routes/api/query.tsx +80 -55
- package/src/routes/api/search-discussion.tsx +169 -0
- package/src/routes/api/status.tsx +18 -10
- package/src/routes/api/sync.tsx +75 -29
- package/src/routes/api/xurl-rate-limits.tsx +24 -0
- package/src/routes/data-sources.tsx +255 -0
- package/src/routes/discuss.tsx +419 -0
- package/src/routes/dms.tsx +95 -28
- package/src/routes/inbox.tsx +32 -19
- package/src/routes/network-map.tsx +1035 -0
- package/src/routes/profile-analyze.tsx +112 -0
- package/src/routes/profiles.$handle.tsx +228 -0
- package/src/routes/rate-limits.tsx +309 -0
- package/src/routes/today.tsx +455 -0
- package/src/styles.css +22 -0
package/src/cli.ts
CHANGED
|
@@ -10,6 +10,9 @@ import { findArchives } from "#/lib/archive-finder";
|
|
|
10
10
|
import {
|
|
11
11
|
ARCHIVE_IMPORT_SLICES,
|
|
12
12
|
type ArchiveImportSlice,
|
|
13
|
+
type ImportProgressEvent,
|
|
14
|
+
type ImportProgressSlice,
|
|
15
|
+
type ImportWritePhase,
|
|
13
16
|
importArchive,
|
|
14
17
|
} from "#/lib/archive-import";
|
|
15
18
|
import {
|
|
@@ -17,6 +20,11 @@ import {
|
|
|
17
20
|
syncAuthoredTweets,
|
|
18
21
|
type AuthoredSyncMode,
|
|
19
22
|
} from "#/lib/authored-live";
|
|
23
|
+
import {
|
|
24
|
+
installAccountSyncLaunchAgent,
|
|
25
|
+
parseAccountSyncSteps,
|
|
26
|
+
runAccountSyncJob,
|
|
27
|
+
} from "#/lib/account-sync-job";
|
|
20
28
|
import {
|
|
21
29
|
exportBackup,
|
|
22
30
|
importBackup,
|
|
@@ -29,24 +37,40 @@ import {
|
|
|
29
37
|
installBookmarkSyncLaunchAgent,
|
|
30
38
|
runBookmarkSyncJob,
|
|
31
39
|
} from "#/lib/bookmark-sync-job";
|
|
40
|
+
import { runDirectMessageRequestMutationViaBird } from "#/lib/bird";
|
|
32
41
|
import { importBlocklist } from "#/lib/blocklist";
|
|
33
42
|
import {
|
|
34
43
|
type ActionsTransport,
|
|
35
44
|
ensureBirdclawDirs,
|
|
36
45
|
getBirdclawPaths,
|
|
37
46
|
resolveMentionsDataSource,
|
|
47
|
+
setActionsTransport,
|
|
38
48
|
} from "#/lib/config";
|
|
39
|
-
import {
|
|
49
|
+
import { closeDatabase } from "#/lib/db";
|
|
50
|
+
import {
|
|
51
|
+
type DirectMessagesSyncMode,
|
|
52
|
+
syncDirectMessagesViaCachedBird,
|
|
53
|
+
} from "#/lib/dms-live";
|
|
40
54
|
import { listInboxItems, scoreInbox } from "#/lib/inbox";
|
|
41
55
|
import { backfillLinkIndex, searchLinks } from "#/lib/link-index";
|
|
42
56
|
import { fetchTweetMedia, formatMediaFetchResult } from "#/lib/media-fetch";
|
|
43
57
|
import { syncMentionThreads } from "#/lib/mention-threads-live";
|
|
44
58
|
import { exportMentionItems } from "#/lib/mentions-export";
|
|
45
59
|
import {
|
|
60
|
+
exportMentionsViaCachedAuto,
|
|
46
61
|
exportMentionsViaCachedBird,
|
|
47
62
|
exportMentionsViaCachedXurl,
|
|
48
63
|
syncMentions,
|
|
49
64
|
} from "#/lib/mentions-live";
|
|
65
|
+
import {
|
|
66
|
+
streamPeriodDigest,
|
|
67
|
+
type PeriodDigestOptions,
|
|
68
|
+
type PeriodDigestPreset,
|
|
69
|
+
} from "#/lib/period-digest";
|
|
70
|
+
import {
|
|
71
|
+
streamProfileAnalysis,
|
|
72
|
+
type ProfileAnalysisOptions,
|
|
73
|
+
} from "#/lib/profile-analysis";
|
|
50
74
|
import {
|
|
51
75
|
getFollowGraphSummary,
|
|
52
76
|
listFollowEvents,
|
|
@@ -61,6 +85,12 @@ import { resolveProfilesForIds } from "#/lib/profile-resolver";
|
|
|
61
85
|
import { inspectProfileReplies } from "#/lib/profile-replies";
|
|
62
86
|
import { runResearchMode } from "#/lib/research";
|
|
63
87
|
import {
|
|
88
|
+
streamSearchDiscussion,
|
|
89
|
+
type SearchDiscussionOptions,
|
|
90
|
+
type SearchDiscussionSource,
|
|
91
|
+
} from "#/lib/search-discussion";
|
|
92
|
+
import {
|
|
93
|
+
applyDmRequestMutationToLocalStore,
|
|
64
94
|
createDmReply,
|
|
65
95
|
createPost,
|
|
66
96
|
createTweetReply,
|
|
@@ -98,6 +128,71 @@ function errorMessage(error: unknown) {
|
|
|
98
128
|
return error instanceof Error ? error.message : String(error);
|
|
99
129
|
}
|
|
100
130
|
|
|
131
|
+
const IMPORT_SLICE_LABELS: Record<ImportProgressSlice, string> = {
|
|
132
|
+
tweets: "tweets",
|
|
133
|
+
noteTweets: "note tweets",
|
|
134
|
+
directMessages: "direct messages",
|
|
135
|
+
likes: "likes",
|
|
136
|
+
bookmarks: "bookmarks",
|
|
137
|
+
media: "media files",
|
|
138
|
+
followers: "followers",
|
|
139
|
+
following: "following",
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const IMPORT_WRITE_LABELS: Record<ImportWritePhase, string> = {
|
|
143
|
+
profiles: "profiles",
|
|
144
|
+
tweets: "tweets",
|
|
145
|
+
collections: "likes+bookmarks",
|
|
146
|
+
dmMessages: "DM messages",
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
function logImportProgress(event: ImportProgressEvent) {
|
|
150
|
+
switch (event.kind) {
|
|
151
|
+
case "scanned":
|
|
152
|
+
process.stderr.write(
|
|
153
|
+
`Scanning archive… ${String(event.entryCount)} entries\n`,
|
|
154
|
+
);
|
|
155
|
+
return;
|
|
156
|
+
case "slice-start":
|
|
157
|
+
if (event.slice === "media") {
|
|
158
|
+
process.stderr.write("Indexing media files…\n");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
process.stderr.write(
|
|
162
|
+
`Parsing ${IMPORT_SLICE_LABELS[event.slice]}… (${String(event.files)} file${event.files === 1 ? "" : "s"})\n`,
|
|
163
|
+
);
|
|
164
|
+
return;
|
|
165
|
+
case "slice-file":
|
|
166
|
+
if (event.files > 1) {
|
|
167
|
+
process.stderr.write(
|
|
168
|
+
` ${IMPORT_SLICE_LABELS[event.slice]} ${String(event.processed)}/${String(event.files)}\n`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
return;
|
|
172
|
+
case "slice-done":
|
|
173
|
+
process.stderr.write(
|
|
174
|
+
` ${IMPORT_SLICE_LABELS[event.slice]}: ${event.count.toLocaleString()}\n`,
|
|
175
|
+
);
|
|
176
|
+
return;
|
|
177
|
+
case "writing":
|
|
178
|
+
process.stderr.write("Writing to database…\n");
|
|
179
|
+
return;
|
|
180
|
+
case "write-start":
|
|
181
|
+
process.stderr.write(
|
|
182
|
+
`Writing ${IMPORT_WRITE_LABELS[event.phase]}… (${event.total.toLocaleString()})\n`,
|
|
183
|
+
);
|
|
184
|
+
return;
|
|
185
|
+
case "write-progress":
|
|
186
|
+
process.stderr.write(
|
|
187
|
+
` ${IMPORT_WRITE_LABELS[event.phase]} ${event.processed.toLocaleString()}/${event.total.toLocaleString()}\n`,
|
|
188
|
+
);
|
|
189
|
+
return;
|
|
190
|
+
case "done":
|
|
191
|
+
process.stderr.write("Import complete.\n");
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
101
196
|
function formatLinkSearchItems(items: ReturnType<typeof searchLinks>) {
|
|
102
197
|
return items
|
|
103
198
|
.map((item) => {
|
|
@@ -154,6 +249,49 @@ function parsePositiveIntegerOption(value: string | undefined, option: string) {
|
|
|
154
249
|
return parsed;
|
|
155
250
|
}
|
|
156
251
|
|
|
252
|
+
function parseDmInboxOption(
|
|
253
|
+
value: string | undefined,
|
|
254
|
+
): "all" | "accepted" | "requests" | undefined {
|
|
255
|
+
const normalized = (value ?? "all").trim().toLowerCase();
|
|
256
|
+
if (
|
|
257
|
+
normalized === "all" ||
|
|
258
|
+
normalized === "accepted" ||
|
|
259
|
+
normalized === "requests"
|
|
260
|
+
) {
|
|
261
|
+
return normalized;
|
|
262
|
+
}
|
|
263
|
+
if (normalized === "request") {
|
|
264
|
+
return "requests";
|
|
265
|
+
}
|
|
266
|
+
printError("--inbox must be all, accepted, or requests");
|
|
267
|
+
process.exitCode = 1;
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function parseDmSyncModeOption(
|
|
272
|
+
value: string | undefined,
|
|
273
|
+
): DirectMessagesSyncMode | undefined {
|
|
274
|
+
const normalized = (value ?? "bird").trim().toLowerCase();
|
|
275
|
+
if (normalized === "auto" || normalized === "bird" || normalized === "xurl") {
|
|
276
|
+
return normalized;
|
|
277
|
+
}
|
|
278
|
+
printError("--mode must be auto, bird, or xurl");
|
|
279
|
+
process.exitCode = 1;
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function parseDigestLiveModeOption(
|
|
284
|
+
value: string | undefined,
|
|
285
|
+
): PeriodDigestOptions["liveSyncMode"] {
|
|
286
|
+
const normalized = (value ?? "xurl").trim().toLowerCase();
|
|
287
|
+
if (normalized === "auto" || normalized === "bird" || normalized === "xurl") {
|
|
288
|
+
return normalized;
|
|
289
|
+
}
|
|
290
|
+
printError("--live-mode must be auto, bird, or xurl");
|
|
291
|
+
process.exitCode = 1;
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
294
|
+
|
|
157
295
|
function parseArchiveImportSelect(value: string | undefined) {
|
|
158
296
|
if (value === undefined) {
|
|
159
297
|
return undefined;
|
|
@@ -209,6 +347,316 @@ function resolveActionOptions(options: { transport?: string }) {
|
|
|
209
347
|
};
|
|
210
348
|
}
|
|
211
349
|
|
|
350
|
+
function parseActionsTransport(value: string | undefined) {
|
|
351
|
+
const normalized = value?.trim().toLowerCase();
|
|
352
|
+
if (normalized === "auto" || normalized === "bird" || normalized === "xurl") {
|
|
353
|
+
return normalized;
|
|
354
|
+
}
|
|
355
|
+
printError("transport must be auto, bird, or xurl");
|
|
356
|
+
process.exitCode = 1;
|
|
357
|
+
return undefined;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function parseDigestPeriod(value: string | undefined): PeriodDigestPreset {
|
|
361
|
+
const normalized = value?.trim().toLowerCase();
|
|
362
|
+
if (normalized === "yesterday") return "yesterday";
|
|
363
|
+
if (normalized === "24h" || normalized === "day") return "24h";
|
|
364
|
+
if (normalized === "week" || normalized === "7d") return "week";
|
|
365
|
+
return "today";
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function buildDigestOptions(
|
|
369
|
+
period: string | undefined,
|
|
370
|
+
options: {
|
|
371
|
+
account?: string;
|
|
372
|
+
includeDms?: boolean;
|
|
373
|
+
model?: string;
|
|
374
|
+
refresh?: boolean;
|
|
375
|
+
since?: string;
|
|
376
|
+
until?: string;
|
|
377
|
+
maxTweets?: string;
|
|
378
|
+
maxLinks?: string;
|
|
379
|
+
liveSync?: boolean;
|
|
380
|
+
liveMode?: string;
|
|
381
|
+
},
|
|
382
|
+
): PeriodDigestOptions | null {
|
|
383
|
+
const maxTweets = parseNonNegativeIntegerOption(
|
|
384
|
+
options.maxTweets,
|
|
385
|
+
"--max-tweets",
|
|
386
|
+
);
|
|
387
|
+
if (options.maxTweets !== undefined && maxTweets === undefined) {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
const maxLinks = parseNonNegativeIntegerOption(
|
|
391
|
+
options.maxLinks,
|
|
392
|
+
"--max-links",
|
|
393
|
+
);
|
|
394
|
+
if (options.maxLinks !== undefined && maxLinks === undefined) {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
const liveSyncMode = parseDigestLiveModeOption(options.liveMode);
|
|
398
|
+
if (liveSyncMode === undefined) {
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
return {
|
|
402
|
+
period: parseDigestPeriod(period),
|
|
403
|
+
since: options.since,
|
|
404
|
+
until: options.until,
|
|
405
|
+
account: options.account,
|
|
406
|
+
includeDms: Boolean(options.includeDms),
|
|
407
|
+
refresh: Boolean(options.refresh),
|
|
408
|
+
model: options.model,
|
|
409
|
+
maxTweets,
|
|
410
|
+
maxLinks,
|
|
411
|
+
liveSync: options.liveSync !== false,
|
|
412
|
+
liveSyncMode,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function runDigestCli(options: PeriodDigestOptions) {
|
|
417
|
+
const asJson = Boolean(program.opts().json);
|
|
418
|
+
return streamPeriodDigest(options, {
|
|
419
|
+
onDelta: asJson
|
|
420
|
+
? undefined
|
|
421
|
+
: (delta) => {
|
|
422
|
+
process.stdout.write(delta);
|
|
423
|
+
},
|
|
424
|
+
}).then((result) => {
|
|
425
|
+
if (asJson) {
|
|
426
|
+
print(result, true);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
if (!result.markdown.endsWith("\n")) {
|
|
430
|
+
process.stdout.write("\n");
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function parseSearchDiscussionSource(
|
|
436
|
+
value: string | undefined,
|
|
437
|
+
): SearchDiscussionSource | undefined {
|
|
438
|
+
const normalized = (value ?? "all").trim().toLowerCase();
|
|
439
|
+
if (
|
|
440
|
+
normalized === "all" ||
|
|
441
|
+
normalized === "home" ||
|
|
442
|
+
normalized === "mentions" ||
|
|
443
|
+
normalized === "authored" ||
|
|
444
|
+
normalized === "search" ||
|
|
445
|
+
normalized === "likes" ||
|
|
446
|
+
normalized === "bookmarks"
|
|
447
|
+
) {
|
|
448
|
+
return normalized;
|
|
449
|
+
}
|
|
450
|
+
printError(
|
|
451
|
+
"--source must be all, search, home, mentions, authored, likes, or bookmarks",
|
|
452
|
+
);
|
|
453
|
+
process.exitCode = 1;
|
|
454
|
+
return undefined;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function parseTweetSearchMode(value: string | undefined) {
|
|
458
|
+
const normalized = (value ?? "auto").trim().toLowerCase();
|
|
459
|
+
if (
|
|
460
|
+
normalized === "auto" ||
|
|
461
|
+
normalized === "bird" ||
|
|
462
|
+
normalized === "xurl" ||
|
|
463
|
+
normalized === "local"
|
|
464
|
+
) {
|
|
465
|
+
return normalized;
|
|
466
|
+
}
|
|
467
|
+
printError("--mode must be auto, bird, xurl, or local");
|
|
468
|
+
process.exitCode = 1;
|
|
469
|
+
return undefined;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function buildSearchDiscussionOptions(
|
|
473
|
+
query: string,
|
|
474
|
+
options: {
|
|
475
|
+
account?: string;
|
|
476
|
+
source?: string;
|
|
477
|
+
includeDms?: boolean;
|
|
478
|
+
since?: string;
|
|
479
|
+
until?: string;
|
|
480
|
+
question?: string;
|
|
481
|
+
originalsOnly?: boolean;
|
|
482
|
+
hideLowQuality?: boolean;
|
|
483
|
+
mode?: string;
|
|
484
|
+
model?: string;
|
|
485
|
+
refresh?: boolean;
|
|
486
|
+
limit?: string;
|
|
487
|
+
maxPages?: string;
|
|
488
|
+
},
|
|
489
|
+
): SearchDiscussionOptions | null {
|
|
490
|
+
const source = parseSearchDiscussionSource(options.source);
|
|
491
|
+
if (!source) return null;
|
|
492
|
+
const mode = parseTweetSearchMode(options.mode);
|
|
493
|
+
if (!mode) return null;
|
|
494
|
+
const limit = parsePositiveIntegerOption(options.limit, "--limit");
|
|
495
|
+
if (options.limit !== undefined && limit === undefined) {
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
const maxPages = parsePositiveIntegerOption(options.maxPages, "--max-pages");
|
|
499
|
+
if (options.maxPages !== undefined && maxPages === undefined) {
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
query,
|
|
504
|
+
account: options.account,
|
|
505
|
+
source,
|
|
506
|
+
includeDms: Boolean(options.includeDms),
|
|
507
|
+
since: options.since,
|
|
508
|
+
until: options.until,
|
|
509
|
+
question: options.question,
|
|
510
|
+
originalsOnly: Boolean(options.originalsOnly),
|
|
511
|
+
hideLowQuality: Boolean(options.hideLowQuality),
|
|
512
|
+
mode,
|
|
513
|
+
model: options.model,
|
|
514
|
+
refresh: Boolean(options.refresh),
|
|
515
|
+
limit,
|
|
516
|
+
maxPages,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function runSearchDiscussionCli(options: SearchDiscussionOptions) {
|
|
521
|
+
const asJson = Boolean(program.opts().json);
|
|
522
|
+
return streamSearchDiscussion(options, {
|
|
523
|
+
onDelta: asJson
|
|
524
|
+
? undefined
|
|
525
|
+
: (delta) => {
|
|
526
|
+
process.stdout.write(delta);
|
|
527
|
+
},
|
|
528
|
+
}).then((result) => {
|
|
529
|
+
if (asJson) {
|
|
530
|
+
print(result, true);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (!result.markdown.endsWith("\n")) {
|
|
534
|
+
process.stdout.write("\n");
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function buildProfileAnalysisOptions(
|
|
540
|
+
handle: string,
|
|
541
|
+
options: {
|
|
542
|
+
account?: string;
|
|
543
|
+
model?: string;
|
|
544
|
+
refresh?: boolean;
|
|
545
|
+
maxTweets?: string;
|
|
546
|
+
maxPages?: string;
|
|
547
|
+
maxConversations?: string;
|
|
548
|
+
maxConversationPages?: string;
|
|
549
|
+
conversationDelayMs?: string;
|
|
550
|
+
rateLimitRetryMs?: string;
|
|
551
|
+
rateLimitRetries?: string;
|
|
552
|
+
},
|
|
553
|
+
): ProfileAnalysisOptions | null {
|
|
554
|
+
const maxTweets = parsePositiveIntegerOption(
|
|
555
|
+
options.maxTweets,
|
|
556
|
+
"--max-tweets",
|
|
557
|
+
);
|
|
558
|
+
if (options.maxTweets !== undefined && maxTweets === undefined) {
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
const maxPages = parsePositiveIntegerOption(options.maxPages, "--max-pages");
|
|
562
|
+
if (options.maxPages !== undefined && maxPages === undefined) {
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
const maxConversations = parsePositiveIntegerOption(
|
|
566
|
+
options.maxConversations,
|
|
567
|
+
"--max-conversations",
|
|
568
|
+
);
|
|
569
|
+
if (
|
|
570
|
+
options.maxConversations !== undefined &&
|
|
571
|
+
maxConversations === undefined
|
|
572
|
+
) {
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
const maxConversationPages = parsePositiveIntegerOption(
|
|
576
|
+
options.maxConversationPages,
|
|
577
|
+
"--max-conversation-pages",
|
|
578
|
+
);
|
|
579
|
+
if (
|
|
580
|
+
options.maxConversationPages !== undefined &&
|
|
581
|
+
maxConversationPages === undefined
|
|
582
|
+
) {
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
const conversationDelayMs = parseNonNegativeIntegerOption(
|
|
586
|
+
options.conversationDelayMs,
|
|
587
|
+
"--conversation-delay-ms",
|
|
588
|
+
);
|
|
589
|
+
if (
|
|
590
|
+
options.conversationDelayMs !== undefined &&
|
|
591
|
+
conversationDelayMs === undefined
|
|
592
|
+
) {
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
const rateLimitRetryMs = parseNonNegativeIntegerOption(
|
|
596
|
+
options.rateLimitRetryMs,
|
|
597
|
+
"--rate-limit-retry-ms",
|
|
598
|
+
);
|
|
599
|
+
if (
|
|
600
|
+
options.rateLimitRetryMs !== undefined &&
|
|
601
|
+
rateLimitRetryMs === undefined
|
|
602
|
+
) {
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
const rateLimitMaxRetries = parseNonNegativeIntegerOption(
|
|
606
|
+
options.rateLimitRetries,
|
|
607
|
+
"--rate-limit-retries",
|
|
608
|
+
);
|
|
609
|
+
if (
|
|
610
|
+
options.rateLimitRetries !== undefined &&
|
|
611
|
+
rateLimitMaxRetries === undefined
|
|
612
|
+
) {
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
return {
|
|
616
|
+
handle,
|
|
617
|
+
account: options.account,
|
|
618
|
+
model: options.model,
|
|
619
|
+
refresh: Boolean(options.refresh),
|
|
620
|
+
maxTweets,
|
|
621
|
+
maxPages,
|
|
622
|
+
maxConversations,
|
|
623
|
+
maxConversationPages,
|
|
624
|
+
conversationDelayMs,
|
|
625
|
+
rateLimitRetryMs,
|
|
626
|
+
rateLimitMaxRetries,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function runProfileAnalysisCli(options: ProfileAnalysisOptions) {
|
|
631
|
+
const asJson = Boolean(program.opts().json);
|
|
632
|
+
return streamProfileAnalysis(options, {
|
|
633
|
+
onDelta: asJson
|
|
634
|
+
? undefined
|
|
635
|
+
: (delta) => {
|
|
636
|
+
process.stdout.write(delta);
|
|
637
|
+
},
|
|
638
|
+
onEvent: asJson
|
|
639
|
+
? undefined
|
|
640
|
+
: (event) => {
|
|
641
|
+
if (event.type === "status") {
|
|
642
|
+
process.stderr.write(
|
|
643
|
+
event.detail
|
|
644
|
+
? `${event.label}: ${event.detail}\n`
|
|
645
|
+
: `${event.label}\n`,
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
},
|
|
649
|
+
}).then((result) => {
|
|
650
|
+
if (asJson) {
|
|
651
|
+
print(result, true);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
if (!result.markdown.endsWith("\n")) {
|
|
655
|
+
process.stdout.write("\n");
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
|
|
212
660
|
async function enrichDmItems(
|
|
213
661
|
query: Parameters<typeof listDmConversations>[0],
|
|
214
662
|
options: {
|
|
@@ -260,14 +708,28 @@ async function enrichDmItems(
|
|
|
260
708
|
}
|
|
261
709
|
|
|
262
710
|
async function autoUpdateBeforeRead() {
|
|
263
|
-
|
|
711
|
+
let result: Awaited<ReturnType<typeof maybeAutoUpdateBackup>>;
|
|
712
|
+
try {
|
|
713
|
+
result = await maybeAutoUpdateBackup();
|
|
714
|
+
} catch (error) {
|
|
715
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
716
|
+
console.error(`birdclaw backup auto-sync failed: ${message}`);
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
264
719
|
if (!result.ok) {
|
|
265
720
|
console.error(`birdclaw backup auto-sync failed: ${result.error}`);
|
|
266
721
|
}
|
|
267
722
|
}
|
|
268
723
|
|
|
269
724
|
async function autoSyncAfterWrite() {
|
|
270
|
-
|
|
725
|
+
let result: Awaited<ReturnType<typeof maybeAutoSyncBackup>>;
|
|
726
|
+
try {
|
|
727
|
+
result = await maybeAutoSyncBackup();
|
|
728
|
+
} catch (error) {
|
|
729
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
730
|
+
console.error(`birdclaw backup sync failed: ${message}`);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
271
733
|
if (!result.ok) {
|
|
272
734
|
console.error(`birdclaw backup sync failed: ${result.error}`);
|
|
273
735
|
}
|
|
@@ -298,14 +760,27 @@ program
|
|
|
298
760
|
);
|
|
299
761
|
});
|
|
300
762
|
|
|
301
|
-
program
|
|
302
|
-
.command("auth
|
|
763
|
+
const authCommand = program
|
|
764
|
+
.command("auth")
|
|
765
|
+
.description("Manage live transport");
|
|
766
|
+
|
|
767
|
+
authCommand
|
|
768
|
+
.command("status")
|
|
303
769
|
.description("Show transport status")
|
|
304
770
|
.action(async () => {
|
|
305
771
|
const meta = await getQueryEnvelope();
|
|
306
772
|
print(meta.transport, program.opts().json ?? false);
|
|
307
773
|
});
|
|
308
774
|
|
|
775
|
+
authCommand
|
|
776
|
+
.command("use <transport>")
|
|
777
|
+
.description("Set preferred moderation action transport")
|
|
778
|
+
.action((transport: string) => {
|
|
779
|
+
const parsed = parseActionsTransport(transport);
|
|
780
|
+
if (!parsed) return;
|
|
781
|
+
print(setActionsTransport(parsed), program.opts().json ?? false);
|
|
782
|
+
});
|
|
783
|
+
|
|
309
784
|
program
|
|
310
785
|
.command("archive find")
|
|
311
786
|
.description("Find likely Twitter archives on disk")
|
|
@@ -342,9 +817,13 @@ importCommand
|
|
|
342
817
|
);
|
|
343
818
|
}
|
|
344
819
|
|
|
345
|
-
const
|
|
820
|
+
const asJson = Boolean(program.opts().json);
|
|
821
|
+
const result = await importArchive(resolvedArchivePath, {
|
|
822
|
+
select,
|
|
823
|
+
onProgress: asJson ? undefined : logImportProgress,
|
|
824
|
+
});
|
|
346
825
|
await autoSyncAfterWrite();
|
|
347
|
-
print(result,
|
|
826
|
+
print(result, asJson);
|
|
348
827
|
});
|
|
349
828
|
|
|
350
829
|
importCommand
|
|
@@ -416,12 +895,13 @@ searchCommand
|
|
|
416
895
|
|
|
417
896
|
searchCommand
|
|
418
897
|
.command("dms <query>")
|
|
898
|
+
.option("--inbox <kind>", "all, accepted, or requests", "all")
|
|
419
899
|
.option("--participant <value>")
|
|
420
900
|
.option("--min-followers <n>", "Minimum sender follower count")
|
|
421
901
|
.option("--max-followers <n>", "Maximum sender follower count")
|
|
422
902
|
.option("--min-influence-score <n>", "Minimum derived influence score")
|
|
423
903
|
.option("--max-influence-score <n>", "Maximum derived influence score")
|
|
424
|
-
.option("--sort <mode>", "recent or
|
|
904
|
+
.option("--sort <mode>", "recent or followers", "recent")
|
|
425
905
|
.option(
|
|
426
906
|
"--context <n>",
|
|
427
907
|
"Include N messages before and after each match",
|
|
@@ -447,6 +927,10 @@ searchCommand
|
|
|
447
927
|
if (context === undefined) {
|
|
448
928
|
return;
|
|
449
929
|
}
|
|
930
|
+
const inbox = parseDmInboxOption(options.inbox);
|
|
931
|
+
if (inbox === undefined) {
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
450
934
|
const replyFilter = options.replied
|
|
451
935
|
? "replied"
|
|
452
936
|
: options.unreplied
|
|
@@ -454,6 +938,7 @@ searchCommand
|
|
|
454
938
|
: "all";
|
|
455
939
|
const dmQuery = {
|
|
456
940
|
search: query,
|
|
941
|
+
...(inbox !== "all" ? { inbox } : {}),
|
|
457
942
|
participant: options.participant,
|
|
458
943
|
minFollowers: options.minFollowers
|
|
459
944
|
? Number(options.minFollowers)
|
|
@@ -467,7 +952,10 @@ searchCommand
|
|
|
467
952
|
maxInfluenceScore: options.maxInfluenceScore
|
|
468
953
|
? Number(options.maxInfluenceScore)
|
|
469
954
|
: undefined,
|
|
470
|
-
sort:
|
|
955
|
+
sort:
|
|
956
|
+
options.sort === "followers" || options.sort === "influence"
|
|
957
|
+
? "followers"
|
|
958
|
+
: "recent",
|
|
471
959
|
replyFilter,
|
|
472
960
|
context,
|
|
473
961
|
limit: Number(options.limit),
|
|
@@ -720,6 +1208,106 @@ program
|
|
|
720
1208
|
);
|
|
721
1209
|
});
|
|
722
1210
|
|
|
1211
|
+
program
|
|
1212
|
+
.command("discuss <query>")
|
|
1213
|
+
.description("Search live/local tweets and summarize the results with AI")
|
|
1214
|
+
.option("--account <accountId>", "Account id")
|
|
1215
|
+
.option(
|
|
1216
|
+
"--source <source>",
|
|
1217
|
+
"all, search, home, mentions, authored, likes, or bookmarks",
|
|
1218
|
+
"search",
|
|
1219
|
+
)
|
|
1220
|
+
.option("--mode <mode>", "auto, bird, xurl, or local", "xurl")
|
|
1221
|
+
.option("--include-dms", "Include private DM search matches")
|
|
1222
|
+
.option("--since <isoDate>", "Include matches created at or after this date")
|
|
1223
|
+
.option("--until <isoDate>", "Include matches created before this date")
|
|
1224
|
+
.option("--question <prompt>", "Discussion question or angle")
|
|
1225
|
+
.option("--originals-only", "Exclude authored replies that start with @")
|
|
1226
|
+
.option("--hide-low-quality", "Hide RTs, tiny replies, and link-only noise")
|
|
1227
|
+
.option("--model <model>", "OpenAI model id")
|
|
1228
|
+
.option("--refresh", "Bypass the local discussion cache")
|
|
1229
|
+
.option("--limit <n>", "Maximum tweet context", "20000")
|
|
1230
|
+
.option("--max-pages <n>", "Maximum live search pages", "200")
|
|
1231
|
+
.action(async (query, options) => {
|
|
1232
|
+
await autoUpdateBeforeRead();
|
|
1233
|
+
const discussionOptions = buildSearchDiscussionOptions(query, options);
|
|
1234
|
+
if (!discussionOptions) return;
|
|
1235
|
+
await runSearchDiscussionCli(discussionOptions);
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
program
|
|
1239
|
+
.command("profile-analyze <handle>")
|
|
1240
|
+
.alias("profile-analyse")
|
|
1241
|
+
.description("Backfill a profile with xurl and summarize it with AI")
|
|
1242
|
+
.option("--account <accountId>", "Account id")
|
|
1243
|
+
.option("--model <model>", "OpenAI model id")
|
|
1244
|
+
.option("--refresh", "Bypass profile fetch and analysis caches")
|
|
1245
|
+
.option("--max-tweets <n>", "Maximum profile tweets", "10000")
|
|
1246
|
+
.option("--max-pages <n>", "Maximum profile timeline pages", "100")
|
|
1247
|
+
.option("--max-conversations <n>", "Maximum conversations to backfill", "80")
|
|
1248
|
+
.option("--max-conversation-pages <n>", "Maximum pages per conversation", "3")
|
|
1249
|
+
.option(
|
|
1250
|
+
"--conversation-delay-ms <n>",
|
|
1251
|
+
"Delay between conversation search calls",
|
|
1252
|
+
)
|
|
1253
|
+
.option(
|
|
1254
|
+
"--rate-limit-retry-ms <n>",
|
|
1255
|
+
"Delay before retrying conversation 429s",
|
|
1256
|
+
)
|
|
1257
|
+
.option("--rate-limit-retries <n>", "Conversation 429 retry count")
|
|
1258
|
+
.action(async (handle, options) => {
|
|
1259
|
+
await autoUpdateBeforeRead();
|
|
1260
|
+
const analysisOptions = buildProfileAnalysisOptions(handle, options);
|
|
1261
|
+
if (!analysisOptions) return;
|
|
1262
|
+
await runProfileAnalysisCli(analysisOptions);
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
program
|
|
1266
|
+
.command("today")
|
|
1267
|
+
.description("Stream an AI digest of what happened today")
|
|
1268
|
+
.option("--account <accountId>", "Account id")
|
|
1269
|
+
.option("--include-dms", "Include private DM context")
|
|
1270
|
+
.option("--model <model>", "OpenAI model id")
|
|
1271
|
+
.option("--refresh", "Bypass the local digest cache")
|
|
1272
|
+
.option("--max-tweets <n>", "Maximum tweet context", "5000")
|
|
1273
|
+
.option("--max-links <n>", "Maximum linked articles", "12")
|
|
1274
|
+
.option("--no-live-sync", "Use only the local database")
|
|
1275
|
+
.option(
|
|
1276
|
+
"--live-mode <mode>",
|
|
1277
|
+
"Live timeline mode: xurl, bird, or auto",
|
|
1278
|
+
"xurl",
|
|
1279
|
+
)
|
|
1280
|
+
.action(async (options) => {
|
|
1281
|
+
await autoUpdateBeforeRead();
|
|
1282
|
+
const digestOptions = buildDigestOptions("today", options);
|
|
1283
|
+
if (!digestOptions) return;
|
|
1284
|
+
await runDigestCli(digestOptions);
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
program
|
|
1288
|
+
.command("digest [period]")
|
|
1289
|
+
.description("Stream an AI digest for today, 24h, yesterday, or week")
|
|
1290
|
+
.option("--account <accountId>", "Account id")
|
|
1291
|
+
.option("--include-dms", "Include private DM context")
|
|
1292
|
+
.option("--since <isoDate>", "Start of explicit window")
|
|
1293
|
+
.option("--until <isoDate>", "End of explicit window")
|
|
1294
|
+
.option("--model <model>", "OpenAI model id")
|
|
1295
|
+
.option("--refresh", "Bypass the local digest cache")
|
|
1296
|
+
.option("--max-tweets <n>", "Maximum tweet context", "5000")
|
|
1297
|
+
.option("--max-links <n>", "Maximum linked articles", "12")
|
|
1298
|
+
.option("--no-live-sync", "Use only the local database")
|
|
1299
|
+
.option(
|
|
1300
|
+
"--live-mode <mode>",
|
|
1301
|
+
"Live timeline mode: xurl, bird, or auto",
|
|
1302
|
+
"xurl",
|
|
1303
|
+
)
|
|
1304
|
+
.action(async (period, options) => {
|
|
1305
|
+
await autoUpdateBeforeRead();
|
|
1306
|
+
const digestOptions = buildDigestOptions(period, options);
|
|
1307
|
+
if (!digestOptions) return;
|
|
1308
|
+
await runDigestCli(digestOptions);
|
|
1309
|
+
});
|
|
1310
|
+
|
|
723
1311
|
const mentionsCommand = program
|
|
724
1312
|
.command("mentions")
|
|
725
1313
|
.description("Export local mention tweets for scripts and agents");
|
|
@@ -728,7 +1316,7 @@ mentionsCommand
|
|
|
728
1316
|
.command("export [query]")
|
|
729
1317
|
.description("Return mention tweets with plain-text and markdown renderings")
|
|
730
1318
|
.option("--account <accountId>", "Account id")
|
|
731
|
-
.option("--mode <mode>", "birdclaw, xurl, or bird")
|
|
1319
|
+
.option("--mode <mode>", "birdclaw, auto, xurl, or bird")
|
|
732
1320
|
.option("--replied", "Only replied items")
|
|
733
1321
|
.option("--unreplied", "Only unreplied items")
|
|
734
1322
|
.option("--refresh", "Refresh the live xurl cache before returning")
|
|
@@ -748,23 +1336,14 @@ mentionsCommand
|
|
|
748
1336
|
: "all";
|
|
749
1337
|
const limit = Number(options.limit);
|
|
750
1338
|
const mode = resolveMentionsDataSource(options.mode);
|
|
751
|
-
if (mode === "xurl") {
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
refresh: Boolean(options.refresh),
|
|
760
|
-
cacheTtlMs: Number(options.cacheTtl) * 1000,
|
|
761
|
-
});
|
|
762
|
-
await autoSyncAfterWrite();
|
|
763
|
-
print(payload, true);
|
|
764
|
-
return;
|
|
765
|
-
}
|
|
766
|
-
if (mode === "bird") {
|
|
767
|
-
const payload = await exportMentionsViaCachedBird({
|
|
1339
|
+
if (mode === "xurl" || mode === "bird" || mode === "auto") {
|
|
1340
|
+
const exportFn =
|
|
1341
|
+
mode === "xurl"
|
|
1342
|
+
? exportMentionsViaCachedXurl
|
|
1343
|
+
: mode === "bird"
|
|
1344
|
+
? exportMentionsViaCachedBird
|
|
1345
|
+
: exportMentionsViaCachedAuto;
|
|
1346
|
+
const payload = await exportFn({
|
|
768
1347
|
account: options.account,
|
|
769
1348
|
search: query,
|
|
770
1349
|
replyFilter,
|
|
@@ -811,16 +1390,20 @@ const syncCommand = program
|
|
|
811
1390
|
|
|
812
1391
|
syncCommand
|
|
813
1392
|
.command("timeline")
|
|
814
|
-
.description("Refresh live home timeline through bird")
|
|
1393
|
+
.description("Refresh live home timeline through xurl or bird")
|
|
815
1394
|
.option("--account <accountId>", "Account id")
|
|
1395
|
+
.option("--mode <mode>", "auto, xurl, or bird", "auto")
|
|
816
1396
|
.option("--limit <n>", "Result limit", "100")
|
|
1397
|
+
.option("--max-pages <n>", "Stop after N xurl pages", "3")
|
|
817
1398
|
.option("--for-you", 'Fetch "For You" instead of chronological Following')
|
|
818
1399
|
.option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
|
|
819
1400
|
.option("--refresh", "Bypass live-cache freshness window")
|
|
820
1401
|
.action(async (options) => {
|
|
821
1402
|
const result = await syncHomeTimeline({
|
|
822
1403
|
account: options.account,
|
|
1404
|
+
mode: options.mode,
|
|
823
1405
|
limit: Number(options.limit),
|
|
1406
|
+
maxPages: Number(options.maxPages),
|
|
824
1407
|
following: !options.forYou,
|
|
825
1408
|
refresh: Boolean(options.refresh),
|
|
826
1409
|
cacheTtlMs: Number(options.cacheTtl) * 1000,
|
|
@@ -833,7 +1416,7 @@ syncCommand
|
|
|
833
1416
|
.command("mentions")
|
|
834
1417
|
.description("Refresh live mentions through xurl or bird")
|
|
835
1418
|
.option("--account <accountId>", "Account id")
|
|
836
|
-
.option("--mode <mode>", "bird or xurl", "
|
|
1419
|
+
.option("--mode <mode>", "auto, bird, or xurl", "auto")
|
|
837
1420
|
.option("--limit <n>", "Result limit per page", "20")
|
|
838
1421
|
.option("--max-pages <n>", "Stop after N pages")
|
|
839
1422
|
.option("--since-id <id>", "Fetch mentions newer than this tweet id")
|
|
@@ -1033,6 +1616,92 @@ const jobsCommand = program
|
|
|
1033
1616
|
.command("jobs")
|
|
1034
1617
|
.description("Run and install background Birdclaw jobs");
|
|
1035
1618
|
|
|
1619
|
+
jobsCommand
|
|
1620
|
+
.command("sync-account")
|
|
1621
|
+
.description("Refresh live account timelines and append a JSONL audit entry")
|
|
1622
|
+
.option("--account <accountId>", "Account id")
|
|
1623
|
+
.option(
|
|
1624
|
+
"--steps <steps>",
|
|
1625
|
+
"Comma list: timeline,mentions,mention-threads,likes,bookmarks,dms",
|
|
1626
|
+
)
|
|
1627
|
+
.option("--mode <mode>", "auto, xurl, or bird for likes/bookmarks", "auto")
|
|
1628
|
+
.option("--limit <n>", "Per-page/result limit", "100")
|
|
1629
|
+
.option("--max-pages <n>", "Stop after N pages", "3")
|
|
1630
|
+
.option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
|
|
1631
|
+
.option("--refresh", "Bypass live-cache freshness window")
|
|
1632
|
+
.option(
|
|
1633
|
+
"--allow-bird-account",
|
|
1634
|
+
"Assert bird cookies match --account for Bird-backed steps",
|
|
1635
|
+
)
|
|
1636
|
+
.option("--log <path>", "Audit JSONL path")
|
|
1637
|
+
.action(async (options) => {
|
|
1638
|
+
const result = await runAccountSyncJob({
|
|
1639
|
+
account: options.account,
|
|
1640
|
+
steps: parseAccountSyncSteps(options.steps),
|
|
1641
|
+
mode: options.mode as TimelineCollectionMode,
|
|
1642
|
+
limit: Number(options.limit),
|
|
1643
|
+
maxPages: Number(options.maxPages),
|
|
1644
|
+
refresh: Boolean(options.refresh),
|
|
1645
|
+
cacheTtlMs: Number(options.cacheTtl) * 1000,
|
|
1646
|
+
allowBirdAccount: Boolean(options.allowBirdAccount),
|
|
1647
|
+
logPath: options.log,
|
|
1648
|
+
});
|
|
1649
|
+
print(result, true);
|
|
1650
|
+
if (!result.ok) {
|
|
1651
|
+
process.exitCode = 1;
|
|
1652
|
+
}
|
|
1653
|
+
});
|
|
1654
|
+
|
|
1655
|
+
jobsCommand
|
|
1656
|
+
.command("install-account-launchd")
|
|
1657
|
+
.description("Install a LaunchAgent that runs account sync")
|
|
1658
|
+
.option("--label <label>", "LaunchAgent label")
|
|
1659
|
+
.option("--interval-seconds <seconds>", "Launch interval", "1800")
|
|
1660
|
+
.option("--program <path>", "birdclaw executable or command", "birdclaw")
|
|
1661
|
+
.option("--account <accountId>", "Account id")
|
|
1662
|
+
.option(
|
|
1663
|
+
"--steps <steps>",
|
|
1664
|
+
"Comma list: timeline,mentions,mention-threads,likes,bookmarks,dms",
|
|
1665
|
+
)
|
|
1666
|
+
.option("--mode <mode>", "auto, xurl, or bird for likes/bookmarks", "auto")
|
|
1667
|
+
.option("--limit <n>", "Per-page/result limit", "100")
|
|
1668
|
+
.option("--max-pages <n>", "Stop after N pages", "3")
|
|
1669
|
+
.option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
|
|
1670
|
+
.option("--no-refresh", "Allow live-cache reuse")
|
|
1671
|
+
.option(
|
|
1672
|
+
"--allow-bird-account",
|
|
1673
|
+
"Assert bird cookies match --account for Bird-backed steps",
|
|
1674
|
+
)
|
|
1675
|
+
.option("--log <path>", "Audit JSONL path")
|
|
1676
|
+
.option("--env-path <path>", "Shell env file to source before running")
|
|
1677
|
+
.option("--env-file <path>", "Deprecated alias for --env-path")
|
|
1678
|
+
.option("--stdout <path>", "launchd stdout path")
|
|
1679
|
+
.option("--stderr <path>", "launchd stderr path")
|
|
1680
|
+
.option("--launch-agents-dir <path>", "LaunchAgents directory")
|
|
1681
|
+
.option("--no-load", "Write plist without loading it")
|
|
1682
|
+
.action(async (options) => {
|
|
1683
|
+
const result = await installAccountSyncLaunchAgent({
|
|
1684
|
+
label: options.label,
|
|
1685
|
+
intervalSeconds: Number(options.intervalSeconds),
|
|
1686
|
+
program: options.program,
|
|
1687
|
+
account: options.account,
|
|
1688
|
+
steps: parseAccountSyncSteps(options.steps),
|
|
1689
|
+
mode: options.mode as TimelineCollectionMode,
|
|
1690
|
+
limit: Number(options.limit),
|
|
1691
|
+
maxPages: Number(options.maxPages),
|
|
1692
|
+
refresh: options.refresh,
|
|
1693
|
+
allowBirdAccount: Boolean(options.allowBirdAccount),
|
|
1694
|
+
cacheTtlSeconds: Number(options.cacheTtl),
|
|
1695
|
+
logPath: options.log,
|
|
1696
|
+
envFile: options.envPath ?? options.envFile,
|
|
1697
|
+
stdoutPath: options.stdout,
|
|
1698
|
+
stderrPath: options.stderr,
|
|
1699
|
+
launchAgentsDir: options.launchAgentsDir,
|
|
1700
|
+
load: options.load,
|
|
1701
|
+
});
|
|
1702
|
+
print(result, true);
|
|
1703
|
+
});
|
|
1704
|
+
|
|
1036
1705
|
jobsCommand
|
|
1037
1706
|
.command("sync-bookmarks")
|
|
1038
1707
|
.description("Refresh live bookmarks and append a JSONL audit entry")
|
|
@@ -1074,7 +1743,8 @@ jobsCommand
|
|
|
1074
1743
|
.option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
|
|
1075
1744
|
.option("--no-refresh", "Allow live-cache reuse")
|
|
1076
1745
|
.option("--log <path>", "Audit JSONL path")
|
|
1077
|
-
.option("--env-
|
|
1746
|
+
.option("--env-path <path>", "Shell env file to source before running")
|
|
1747
|
+
.option("--env-file <path>", "Deprecated alias for --env-path")
|
|
1078
1748
|
.option("--stdout <path>", "launchd stdout path")
|
|
1079
1749
|
.option("--stderr <path>", "launchd stderr path")
|
|
1080
1750
|
.option("--launch-agents-dir <path>", "LaunchAgents directory")
|
|
@@ -1091,7 +1761,7 @@ jobsCommand
|
|
|
1091
1761
|
refresh: options.refresh,
|
|
1092
1762
|
cacheTtlSeconds: Number(options.cacheTtl),
|
|
1093
1763
|
logPath: options.log,
|
|
1094
|
-
envFile: options.envFile,
|
|
1764
|
+
envFile: options.envPath ?? options.envFile,
|
|
1095
1765
|
stdoutPath: options.stdout,
|
|
1096
1766
|
stderrPath: options.stderr,
|
|
1097
1767
|
launchAgentsDir: options.launchAgentsDir,
|
|
@@ -1103,14 +1773,19 @@ jobsCommand
|
|
|
1103
1773
|
dmsCommand
|
|
1104
1774
|
.command("list")
|
|
1105
1775
|
.option("--account <accountId>", "Account id")
|
|
1106
|
-
.option("--
|
|
1776
|
+
.option("--mode <mode>", "auto, bird, or xurl", "bird")
|
|
1777
|
+
.option("--refresh", "Refresh live DMs before listing")
|
|
1107
1778
|
.option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
|
|
1779
|
+
.option("--inbox <kind>", "all, accepted, or requests", "all")
|
|
1780
|
+
.option("--max-pages <n>", "Additional accepted/request pages to sync", "0")
|
|
1781
|
+
.option("--all-pages", "Fetch all accepted/request pages while syncing")
|
|
1782
|
+
.option("--page-delay-ms <n>", "Delay between live DM page requests", "0")
|
|
1108
1783
|
.option("--participant <value>")
|
|
1109
1784
|
.option("--min-followers <n>", "Minimum sender follower count")
|
|
1110
1785
|
.option("--max-followers <n>", "Maximum sender follower count")
|
|
1111
1786
|
.option("--min-influence-score <n>", "Minimum derived influence score")
|
|
1112
1787
|
.option("--max-influence-score <n>", "Maximum derived influence score")
|
|
1113
|
-
.option("--sort <mode>", "recent or
|
|
1788
|
+
.option("--sort <mode>", "recent or followers", "recent")
|
|
1114
1789
|
.option(
|
|
1115
1790
|
"--resolve-profiles",
|
|
1116
1791
|
"Resolve placeholder DM profiles through cache/bird/xurl",
|
|
@@ -1131,10 +1806,33 @@ dmsCommand
|
|
|
1131
1806
|
: options.unreplied
|
|
1132
1807
|
? "unreplied"
|
|
1133
1808
|
: "all";
|
|
1809
|
+
const inbox = parseDmInboxOption(options.inbox);
|
|
1810
|
+
const mode = parseDmSyncModeOption(options.mode);
|
|
1811
|
+
const maxPages = parseNonNegativeIntegerOption(
|
|
1812
|
+
options.maxPages,
|
|
1813
|
+
"--max-pages",
|
|
1814
|
+
);
|
|
1815
|
+
const pageDelayMs = parseNonNegativeIntegerOption(
|
|
1816
|
+
options.pageDelayMs,
|
|
1817
|
+
"--page-delay-ms",
|
|
1818
|
+
);
|
|
1819
|
+
if (
|
|
1820
|
+
inbox === undefined ||
|
|
1821
|
+
mode === undefined ||
|
|
1822
|
+
maxPages === undefined ||
|
|
1823
|
+
pageDelayMs === undefined
|
|
1824
|
+
) {
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1134
1827
|
if (options.refresh) {
|
|
1135
1828
|
await syncDirectMessagesViaCachedBird({
|
|
1136
1829
|
account: options.account,
|
|
1830
|
+
mode,
|
|
1137
1831
|
limit: Number(options.limit),
|
|
1832
|
+
...(inbox !== "all" ? { inbox } : {}),
|
|
1833
|
+
...(maxPages > 0 ? { maxPages } : {}),
|
|
1834
|
+
...(options.allPages ? { allPages: true } : {}),
|
|
1835
|
+
...(pageDelayMs > 0 ? { pageDelayMs } : {}),
|
|
1138
1836
|
refresh: true,
|
|
1139
1837
|
cacheTtlMs: Number(options.cacheTtl) * 1000,
|
|
1140
1838
|
});
|
|
@@ -1145,6 +1843,7 @@ dmsCommand
|
|
|
1145
1843
|
const items = await enrichDmItems(
|
|
1146
1844
|
{
|
|
1147
1845
|
account: options.account,
|
|
1846
|
+
...(inbox !== "all" ? { inbox } : {}),
|
|
1148
1847
|
participant: options.participant,
|
|
1149
1848
|
minFollowers: options.minFollowers
|
|
1150
1849
|
? Number(options.minFollowers)
|
|
@@ -1158,7 +1857,10 @@ dmsCommand
|
|
|
1158
1857
|
maxInfluenceScore: options.maxInfluenceScore
|
|
1159
1858
|
? Number(options.maxInfluenceScore)
|
|
1160
1859
|
: undefined,
|
|
1161
|
-
sort:
|
|
1860
|
+
sort:
|
|
1861
|
+
options.sort === "followers" || options.sort === "influence"
|
|
1862
|
+
? "followers"
|
|
1863
|
+
: "recent",
|
|
1162
1864
|
replyFilter,
|
|
1163
1865
|
limit: Number(options.limit),
|
|
1164
1866
|
},
|
|
@@ -1175,15 +1877,43 @@ dmsCommand
|
|
|
1175
1877
|
|
|
1176
1878
|
dmsCommand
|
|
1177
1879
|
.command("sync")
|
|
1178
|
-
.description("Refresh live direct messages
|
|
1880
|
+
.description("Refresh live direct messages into the local store")
|
|
1179
1881
|
.option("--account <accountId>", "Account id")
|
|
1882
|
+
.option("--mode <mode>", "auto, bird, or xurl", "bird")
|
|
1180
1883
|
.option("--limit <n>", "Limit messages", "20")
|
|
1884
|
+
.option("--inbox <kind>", "all, accepted, or requests", "all")
|
|
1885
|
+
.option("--max-pages <n>", "Additional accepted/request pages to sync", "0")
|
|
1886
|
+
.option("--all-pages", "Fetch all accepted/request pages")
|
|
1887
|
+
.option("--page-delay-ms <n>", "Delay between live DM page requests", "0")
|
|
1181
1888
|
.option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
|
|
1182
1889
|
.option("--refresh", "Bypass live-cache freshness window")
|
|
1183
1890
|
.action(async (options) => {
|
|
1891
|
+
const inbox = parseDmInboxOption(options.inbox);
|
|
1892
|
+
const mode = parseDmSyncModeOption(options.mode);
|
|
1893
|
+
const maxPages = parseNonNegativeIntegerOption(
|
|
1894
|
+
options.maxPages,
|
|
1895
|
+
"--max-pages",
|
|
1896
|
+
);
|
|
1897
|
+
const pageDelayMs = parseNonNegativeIntegerOption(
|
|
1898
|
+
options.pageDelayMs,
|
|
1899
|
+
"--page-delay-ms",
|
|
1900
|
+
);
|
|
1901
|
+
if (
|
|
1902
|
+
inbox === undefined ||
|
|
1903
|
+
mode === undefined ||
|
|
1904
|
+
maxPages === undefined ||
|
|
1905
|
+
pageDelayMs === undefined
|
|
1906
|
+
) {
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1184
1909
|
const result = await syncDirectMessagesViaCachedBird({
|
|
1185
1910
|
account: options.account,
|
|
1911
|
+
mode,
|
|
1186
1912
|
limit: Number(options.limit),
|
|
1913
|
+
...(inbox !== "all" ? { inbox } : {}),
|
|
1914
|
+
...(maxPages > 0 ? { maxPages } : {}),
|
|
1915
|
+
...(options.allPages ? { allPages: true } : {}),
|
|
1916
|
+
...(pageDelayMs > 0 ? { pageDelayMs } : {}),
|
|
1187
1917
|
refresh: Boolean(options.refresh),
|
|
1188
1918
|
cacheTtlMs: Number(options.cacheTtl) * 1000,
|
|
1189
1919
|
});
|
|
@@ -1191,6 +1921,39 @@ dmsCommand
|
|
|
1191
1921
|
print(result, true);
|
|
1192
1922
|
});
|
|
1193
1923
|
|
|
1924
|
+
for (const action of ["accept", "reject", "block"] as const) {
|
|
1925
|
+
const command = dmsCommand
|
|
1926
|
+
.command(`${action} <conversationId>`)
|
|
1927
|
+
.description(`${action} a live DM message request through bird`);
|
|
1928
|
+
if (action === "block") {
|
|
1929
|
+
command
|
|
1930
|
+
.option("--max-pages <n>", "Additional timeline pages to search", "3")
|
|
1931
|
+
.option("--all-pages", "Search all accepted/request timeline pages");
|
|
1932
|
+
}
|
|
1933
|
+
command.action(async (conversationId, options) => {
|
|
1934
|
+
const maxPages =
|
|
1935
|
+
action === "block"
|
|
1936
|
+
? parseNonNegativeIntegerOption(options.maxPages, "--max-pages")
|
|
1937
|
+
: undefined;
|
|
1938
|
+
if (action === "block" && maxPages === undefined) {
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
const result = await runDirectMessageRequestMutationViaBird({
|
|
1942
|
+
action,
|
|
1943
|
+
conversationId,
|
|
1944
|
+
...(action === "block" && maxPages !== undefined ? { maxPages } : {}),
|
|
1945
|
+
...(action === "block" && options.allPages ? { allPages: true } : {}),
|
|
1946
|
+
});
|
|
1947
|
+
if (result.success) {
|
|
1948
|
+
applyDmRequestMutationToLocalStore(conversationId, action);
|
|
1949
|
+
} else {
|
|
1950
|
+
process.exitCode = 1;
|
|
1951
|
+
}
|
|
1952
|
+
await autoSyncAfterWrite();
|
|
1953
|
+
print(result, true);
|
|
1954
|
+
});
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1194
1957
|
registerModerationCommands({
|
|
1195
1958
|
program,
|
|
1196
1959
|
print,
|
|
@@ -1470,9 +2233,17 @@ program
|
|
|
1470
2233
|
await autoUpdateBeforeRead();
|
|
1471
2234
|
const child = spawn(
|
|
1472
2235
|
process.execPath,
|
|
1473
|
-
[
|
|
2236
|
+
[
|
|
2237
|
+
"node_modules/vite/bin/vite.js",
|
|
2238
|
+
"dev",
|
|
2239
|
+
"--host",
|
|
2240
|
+
"127.0.0.1",
|
|
2241
|
+
"--port",
|
|
2242
|
+
"3000",
|
|
2243
|
+
],
|
|
1474
2244
|
{
|
|
1475
2245
|
cwd: packageRoot,
|
|
2246
|
+
env: { ...process.env, BIRDCLAW_LOCAL_WEB: "1" },
|
|
1476
2247
|
stdio: "inherit",
|
|
1477
2248
|
detached: process.platform !== "win32",
|
|
1478
2249
|
},
|
|
@@ -1527,7 +2298,11 @@ program
|
|
|
1527
2298
|
});
|
|
1528
2299
|
|
|
1529
2300
|
export async function runCli(argv = process.argv) {
|
|
1530
|
-
|
|
2301
|
+
try {
|
|
2302
|
+
await program.parseAsync(argv);
|
|
2303
|
+
} finally {
|
|
2304
|
+
await closeDatabase();
|
|
2305
|
+
}
|
|
1531
2306
|
}
|
|
1532
2307
|
|
|
1533
2308
|
/* v8 ignore next 5 */
|