birdclaw 0.6.0 → 0.8.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 +64 -0
- package/README.md +32 -2
- package/package.json +29 -30
- package/scripts/build-docs-site.mjs +39 -12
- package/src/cli.ts +457 -26
- package/src/components/AppNav.tsx +10 -0
- package/src/components/MarkdownViewer.tsx +438 -72
- 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 +5 -2
- package/src/components/TimelineCard.tsx +20 -4
- package/src/components/TimelineRouteFrame.tsx +16 -0
- package/src/components/TweetRichText.tsx +36 -12
- package/src/components/useTimelineRouteData.ts +74 -6
- package/src/lib/account-sync-job.ts +15 -3
- package/src/lib/archive-finder.ts +1 -1
- package/src/lib/archive-import.ts +245 -7
- package/src/lib/authored-live.ts +1 -0
- package/src/lib/avatar-cache.ts +50 -0
- package/src/lib/backup.ts +4 -3
- package/src/lib/bird.ts +33 -0
- package/src/lib/config.ts +35 -2
- package/src/lib/data-sources.ts +219 -0
- package/src/lib/db.ts +62 -1
- package/src/lib/geocoding.ts +296 -0
- package/src/lib/link-insights.ts +2 -0
- package/src/lib/location.ts +137 -0
- package/src/lib/mention-threads-live.ts +94 -1
- package/src/lib/mentions-live.ts +187 -40
- package/src/lib/network-map.ts +382 -0
- package/src/lib/period-digest.ts +468 -29
- package/src/lib/profile-analysis.ts +1272 -0
- package/src/lib/profile-bio-entities.ts +1 -1
- package/src/lib/queries.ts +14 -4
- package/src/lib/search-discussion.ts +1016 -0
- package/src/lib/timeline-live.ts +272 -19
- package/src/lib/tweet-account-edges.ts +2 -0
- package/src/lib/tweet-render.ts +141 -1
- package/src/lib/tweet-search-live.ts +565 -0
- package/src/lib/types.ts +37 -2
- package/src/lib/ui.ts +1 -1
- package/src/lib/web-sync.ts +7 -2
- package/src/lib/xurl-rate-limits.ts +267 -0
- package/src/lib/xurl.ts +551 -41
- package/src/routeTree.gen.ts +231 -0
- package/src/routes/__root.tsx +5 -6
- package/src/routes/api/data-sources.tsx +24 -0
- package/src/routes/api/network-map.tsx +55 -0
- package/src/routes/api/period-digest.tsx +29 -3
- package/src/routes/api/profile-analysis.tsx +152 -0
- package/src/routes/api/query.tsx +1 -0
- package/src/routes/api/search-discussion.tsx +169 -0
- package/src/routes/api/xurl-rate-limits.tsx +24 -0
- package/src/routes/data-sources.tsx +257 -0
- package/src/routes/discuss.tsx +419 -0
- 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 +22 -8
- 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 {
|
|
@@ -41,6 +44,7 @@ import {
|
|
|
41
44
|
ensureBirdclawDirs,
|
|
42
45
|
getBirdclawPaths,
|
|
43
46
|
resolveMentionsDataSource,
|
|
47
|
+
setActionsTransport,
|
|
44
48
|
} from "#/lib/config";
|
|
45
49
|
import { closeDatabase } from "#/lib/db";
|
|
46
50
|
import {
|
|
@@ -53,15 +57,21 @@ import { fetchTweetMedia, formatMediaFetchResult } from "#/lib/media-fetch";
|
|
|
53
57
|
import { syncMentionThreads } from "#/lib/mention-threads-live";
|
|
54
58
|
import { exportMentionItems } from "#/lib/mentions-export";
|
|
55
59
|
import {
|
|
60
|
+
exportMentionsViaCachedAuto,
|
|
56
61
|
exportMentionsViaCachedBird,
|
|
57
62
|
exportMentionsViaCachedXurl,
|
|
58
63
|
syncMentions,
|
|
59
64
|
} from "#/lib/mentions-live";
|
|
60
65
|
import {
|
|
66
|
+
normalizeDigestLanguage,
|
|
61
67
|
streamPeriodDigest,
|
|
62
68
|
type PeriodDigestOptions,
|
|
63
69
|
type PeriodDigestPreset,
|
|
64
70
|
} from "#/lib/period-digest";
|
|
71
|
+
import {
|
|
72
|
+
streamProfileAnalysis,
|
|
73
|
+
type ProfileAnalysisOptions,
|
|
74
|
+
} from "#/lib/profile-analysis";
|
|
65
75
|
import {
|
|
66
76
|
getFollowGraphSummary,
|
|
67
77
|
listFollowEvents,
|
|
@@ -75,6 +85,11 @@ import { hydrateProfilesFromX } from "#/lib/profile-hydration";
|
|
|
75
85
|
import { resolveProfilesForIds } from "#/lib/profile-resolver";
|
|
76
86
|
import { inspectProfileReplies } from "#/lib/profile-replies";
|
|
77
87
|
import { runResearchMode } from "#/lib/research";
|
|
88
|
+
import {
|
|
89
|
+
streamSearchDiscussion,
|
|
90
|
+
type SearchDiscussionOptions,
|
|
91
|
+
type SearchDiscussionSource,
|
|
92
|
+
} from "#/lib/search-discussion";
|
|
78
93
|
import {
|
|
79
94
|
applyDmRequestMutationToLocalStore,
|
|
80
95
|
createDmReply,
|
|
@@ -114,6 +129,71 @@ function errorMessage(error: unknown) {
|
|
|
114
129
|
return error instanceof Error ? error.message : String(error);
|
|
115
130
|
}
|
|
116
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
|
+
|
|
117
197
|
function formatLinkSearchItems(items: ReturnType<typeof searchLinks>) {
|
|
118
198
|
return items
|
|
119
199
|
.map((item) => {
|
|
@@ -201,6 +281,18 @@ function parseDmSyncModeOption(
|
|
|
201
281
|
return undefined;
|
|
202
282
|
}
|
|
203
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
|
+
|
|
204
296
|
function parseArchiveImportSelect(value: string | undefined) {
|
|
205
297
|
if (value === undefined) {
|
|
206
298
|
return undefined;
|
|
@@ -256,6 +348,16 @@ function resolveActionOptions(options: { transport?: string }) {
|
|
|
256
348
|
};
|
|
257
349
|
}
|
|
258
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
|
+
|
|
259
361
|
function parseDigestPeriod(value: string | undefined): PeriodDigestPreset {
|
|
260
362
|
const normalized = value?.trim().toLowerCase();
|
|
261
363
|
if (normalized === "yesterday") return "yesterday";
|
|
@@ -270,11 +372,14 @@ function buildDigestOptions(
|
|
|
270
372
|
account?: string;
|
|
271
373
|
includeDms?: boolean;
|
|
272
374
|
model?: string;
|
|
375
|
+
language?: string;
|
|
273
376
|
refresh?: boolean;
|
|
274
377
|
since?: string;
|
|
275
378
|
until?: string;
|
|
276
379
|
maxTweets?: string;
|
|
277
380
|
maxLinks?: string;
|
|
381
|
+
liveSync?: boolean;
|
|
382
|
+
liveMode?: string;
|
|
278
383
|
},
|
|
279
384
|
): PeriodDigestOptions | null {
|
|
280
385
|
const maxTweets = parseNonNegativeIntegerOption(
|
|
@@ -291,6 +396,18 @@ function buildDigestOptions(
|
|
|
291
396
|
if (options.maxLinks !== undefined && maxLinks === undefined) {
|
|
292
397
|
return null;
|
|
293
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
|
+
}
|
|
294
411
|
return {
|
|
295
412
|
period: parseDigestPeriod(period),
|
|
296
413
|
since: options.since,
|
|
@@ -299,8 +416,11 @@ function buildDigestOptions(
|
|
|
299
416
|
includeDms: Boolean(options.includeDms),
|
|
300
417
|
refresh: Boolean(options.refresh),
|
|
301
418
|
model: options.model,
|
|
419
|
+
language,
|
|
302
420
|
maxTweets,
|
|
303
421
|
maxLinks,
|
|
422
|
+
liveSync: options.liveSync !== false,
|
|
423
|
+
liveSyncMode,
|
|
304
424
|
};
|
|
305
425
|
}
|
|
306
426
|
|
|
@@ -323,6 +443,231 @@ function runDigestCli(options: PeriodDigestOptions) {
|
|
|
323
443
|
});
|
|
324
444
|
}
|
|
325
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
|
+
|
|
326
671
|
async function enrichDmItems(
|
|
327
672
|
query: Parameters<typeof listDmConversations>[0],
|
|
328
673
|
options: {
|
|
@@ -426,14 +771,27 @@ program
|
|
|
426
771
|
);
|
|
427
772
|
});
|
|
428
773
|
|
|
429
|
-
program
|
|
430
|
-
.command("auth
|
|
774
|
+
const authCommand = program
|
|
775
|
+
.command("auth")
|
|
776
|
+
.description("Manage live transport");
|
|
777
|
+
|
|
778
|
+
authCommand
|
|
779
|
+
.command("status")
|
|
431
780
|
.description("Show transport status")
|
|
432
781
|
.action(async () => {
|
|
433
782
|
const meta = await getQueryEnvelope();
|
|
434
783
|
print(meta.transport, program.opts().json ?? false);
|
|
435
784
|
});
|
|
436
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
|
+
|
|
437
795
|
program
|
|
438
796
|
.command("archive find")
|
|
439
797
|
.description("Find likely Twitter archives on disk")
|
|
@@ -470,9 +828,13 @@ importCommand
|
|
|
470
828
|
);
|
|
471
829
|
}
|
|
472
830
|
|
|
473
|
-
const
|
|
831
|
+
const asJson = Boolean(program.opts().json);
|
|
832
|
+
const result = await importArchive(resolvedArchivePath, {
|
|
833
|
+
select,
|
|
834
|
+
onProgress: asJson ? undefined : logImportProgress,
|
|
835
|
+
});
|
|
474
836
|
await autoSyncAfterWrite();
|
|
475
|
-
print(result,
|
|
837
|
+
print(result, asJson);
|
|
476
838
|
});
|
|
477
839
|
|
|
478
840
|
importCommand
|
|
@@ -857,15 +1219,79 @@ program
|
|
|
857
1219
|
);
|
|
858
1220
|
});
|
|
859
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
|
+
|
|
860
1276
|
program
|
|
861
1277
|
.command("today")
|
|
862
1278
|
.description("Stream an AI digest of what happened today")
|
|
863
1279
|
.option("--account <accountId>", "Account id")
|
|
864
1280
|
.option("--include-dms", "Include private DM context")
|
|
865
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
|
+
)
|
|
866
1286
|
.option("--refresh", "Bypass the local digest cache")
|
|
867
|
-
.option("--max-tweets <n>", "Maximum tweet context", "
|
|
1287
|
+
.option("--max-tweets <n>", "Maximum tweet context", "5000")
|
|
868
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
|
+
)
|
|
869
1295
|
.action(async (options) => {
|
|
870
1296
|
await autoUpdateBeforeRead();
|
|
871
1297
|
const digestOptions = buildDigestOptions("today", options);
|
|
@@ -881,9 +1307,19 @@ program
|
|
|
881
1307
|
.option("--since <isoDate>", "Start of explicit window")
|
|
882
1308
|
.option("--until <isoDate>", "End of explicit window")
|
|
883
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
|
+
)
|
|
884
1314
|
.option("--refresh", "Bypass the local digest cache")
|
|
885
|
-
.option("--max-tweets <n>", "Maximum tweet context", "
|
|
1315
|
+
.option("--max-tweets <n>", "Maximum tweet context", "5000")
|
|
886
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
|
+
)
|
|
887
1323
|
.action(async (period, options) => {
|
|
888
1324
|
await autoUpdateBeforeRead();
|
|
889
1325
|
const digestOptions = buildDigestOptions(period, options);
|
|
@@ -899,7 +1335,7 @@ mentionsCommand
|
|
|
899
1335
|
.command("export [query]")
|
|
900
1336
|
.description("Return mention tweets with plain-text and markdown renderings")
|
|
901
1337
|
.option("--account <accountId>", "Account id")
|
|
902
|
-
.option("--mode <mode>", "birdclaw, xurl, or bird")
|
|
1338
|
+
.option("--mode <mode>", "birdclaw, auto, xurl, or bird")
|
|
903
1339
|
.option("--replied", "Only replied items")
|
|
904
1340
|
.option("--unreplied", "Only unreplied items")
|
|
905
1341
|
.option("--refresh", "Refresh the live xurl cache before returning")
|
|
@@ -919,23 +1355,14 @@ mentionsCommand
|
|
|
919
1355
|
: "all";
|
|
920
1356
|
const limit = Number(options.limit);
|
|
921
1357
|
const mode = resolveMentionsDataSource(options.mode);
|
|
922
|
-
if (mode === "xurl") {
|
|
923
|
-
const
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
refresh: Boolean(options.refresh),
|
|
931
|
-
cacheTtlMs: Number(options.cacheTtl) * 1000,
|
|
932
|
-
});
|
|
933
|
-
await autoSyncAfterWrite();
|
|
934
|
-
print(payload, true);
|
|
935
|
-
return;
|
|
936
|
-
}
|
|
937
|
-
if (mode === "bird") {
|
|
938
|
-
const payload = await exportMentionsViaCachedBird({
|
|
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({
|
|
939
1366
|
account: options.account,
|
|
940
1367
|
search: query,
|
|
941
1368
|
replyFilter,
|
|
@@ -982,16 +1409,20 @@ const syncCommand = program
|
|
|
982
1409
|
|
|
983
1410
|
syncCommand
|
|
984
1411
|
.command("timeline")
|
|
985
|
-
.description("Refresh live home timeline through bird")
|
|
1412
|
+
.description("Refresh live home timeline through xurl or bird")
|
|
986
1413
|
.option("--account <accountId>", "Account id")
|
|
1414
|
+
.option("--mode <mode>", "auto, xurl, or bird", "auto")
|
|
987
1415
|
.option("--limit <n>", "Result limit", "100")
|
|
1416
|
+
.option("--max-pages <n>", "Stop after N xurl pages", "3")
|
|
988
1417
|
.option("--for-you", 'Fetch "For You" instead of chronological Following')
|
|
989
1418
|
.option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
|
|
990
1419
|
.option("--refresh", "Bypass live-cache freshness window")
|
|
991
1420
|
.action(async (options) => {
|
|
992
1421
|
const result = await syncHomeTimeline({
|
|
993
1422
|
account: options.account,
|
|
1423
|
+
mode: options.mode,
|
|
994
1424
|
limit: Number(options.limit),
|
|
1425
|
+
maxPages: Number(options.maxPages),
|
|
995
1426
|
following: !options.forYou,
|
|
996
1427
|
refresh: Boolean(options.refresh),
|
|
997
1428
|
cacheTtlMs: Number(options.cacheTtl) * 1000,
|
|
@@ -1004,7 +1435,7 @@ syncCommand
|
|
|
1004
1435
|
.command("mentions")
|
|
1005
1436
|
.description("Refresh live mentions through xurl or bird")
|
|
1006
1437
|
.option("--account <accountId>", "Account id")
|
|
1007
|
-
.option("--mode <mode>", "bird or xurl", "
|
|
1438
|
+
.option("--mode <mode>", "auto, bird, or xurl", "auto")
|
|
1008
1439
|
.option("--limit <n>", "Result limit per page", "20")
|
|
1009
1440
|
.option("--max-pages <n>", "Stop after N pages")
|
|
1010
1441
|
.option("--since-id <id>", "Fetch mentions newer than this tweet id")
|
|
@@ -3,12 +3,17 @@ import {
|
|
|
3
3
|
Bell,
|
|
4
4
|
Bookmark,
|
|
5
5
|
CalendarDays,
|
|
6
|
+
Database,
|
|
7
|
+
Gauge,
|
|
8
|
+
Globe2,
|
|
6
9
|
Heart,
|
|
7
10
|
Home,
|
|
8
11
|
Inbox,
|
|
9
12
|
Link as LinkIcon,
|
|
10
13
|
Mail,
|
|
14
|
+
MessagesSquare,
|
|
11
15
|
ShieldOff,
|
|
16
|
+
UserSearch,
|
|
12
17
|
} from "lucide-react";
|
|
13
18
|
import {
|
|
14
19
|
cx,
|
|
@@ -36,11 +41,16 @@ import { ThemeSlider } from "./ThemeSlider";
|
|
|
36
41
|
const links = [
|
|
37
42
|
{ to: "/inbox", label: "Inbox", icon: Inbox },
|
|
38
43
|
{ to: "/today", label: "Today", icon: CalendarDays },
|
|
44
|
+
{ to: "/discuss", label: "Discuss", icon: MessagesSquare },
|
|
45
|
+
{ to: "/profile-analyze", label: "Analyse", icon: UserSearch },
|
|
46
|
+
{ to: "/network-map", label: "Map", icon: Globe2 },
|
|
47
|
+
{ to: "/data-sources", label: "Sources", icon: Database },
|
|
39
48
|
{ to: "/", label: "Home", icon: Home },
|
|
40
49
|
{ to: "/mentions", label: "Mentions", icon: Bell },
|
|
41
50
|
{ to: "/likes", label: "Likes", icon: Heart },
|
|
42
51
|
{ to: "/bookmarks", label: "Bookmarks", icon: Bookmark },
|
|
43
52
|
{ to: "/links", label: "Links", icon: LinkIcon },
|
|
53
|
+
{ to: "/rate-limits", label: "Rate Limits", icon: Gauge },
|
|
44
54
|
{ to: "/dms", label: "DMs", icon: Mail },
|
|
45
55
|
{ to: "/blocks", label: "Blocks", icon: ShieldOff },
|
|
46
56
|
] as const;
|