birdclaw 0.6.0 → 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 +48 -0
- package/README.md +25 -0
- package/package.json +6 -1
- package/src/cli.ts +438 -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/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 +377 -13
- 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 +11 -1
- 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 +255 -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,6 +57,7 @@ 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,
|
|
@@ -62,6 +67,10 @@ import {
|
|
|
62
67
|
type PeriodDigestOptions,
|
|
63
68
|
type PeriodDigestPreset,
|
|
64
69
|
} from "#/lib/period-digest";
|
|
70
|
+
import {
|
|
71
|
+
streamProfileAnalysis,
|
|
72
|
+
type ProfileAnalysisOptions,
|
|
73
|
+
} from "#/lib/profile-analysis";
|
|
65
74
|
import {
|
|
66
75
|
getFollowGraphSummary,
|
|
67
76
|
listFollowEvents,
|
|
@@ -75,6 +84,11 @@ import { hydrateProfilesFromX } from "#/lib/profile-hydration";
|
|
|
75
84
|
import { resolveProfilesForIds } from "#/lib/profile-resolver";
|
|
76
85
|
import { inspectProfileReplies } from "#/lib/profile-replies";
|
|
77
86
|
import { runResearchMode } from "#/lib/research";
|
|
87
|
+
import {
|
|
88
|
+
streamSearchDiscussion,
|
|
89
|
+
type SearchDiscussionOptions,
|
|
90
|
+
type SearchDiscussionSource,
|
|
91
|
+
} from "#/lib/search-discussion";
|
|
78
92
|
import {
|
|
79
93
|
applyDmRequestMutationToLocalStore,
|
|
80
94
|
createDmReply,
|
|
@@ -114,6 +128,71 @@ function errorMessage(error: unknown) {
|
|
|
114
128
|
return error instanceof Error ? error.message : String(error);
|
|
115
129
|
}
|
|
116
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
|
+
|
|
117
196
|
function formatLinkSearchItems(items: ReturnType<typeof searchLinks>) {
|
|
118
197
|
return items
|
|
119
198
|
.map((item) => {
|
|
@@ -201,6 +280,18 @@ function parseDmSyncModeOption(
|
|
|
201
280
|
return undefined;
|
|
202
281
|
}
|
|
203
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
|
+
|
|
204
295
|
function parseArchiveImportSelect(value: string | undefined) {
|
|
205
296
|
if (value === undefined) {
|
|
206
297
|
return undefined;
|
|
@@ -256,6 +347,16 @@ function resolveActionOptions(options: { transport?: string }) {
|
|
|
256
347
|
};
|
|
257
348
|
}
|
|
258
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
|
+
|
|
259
360
|
function parseDigestPeriod(value: string | undefined): PeriodDigestPreset {
|
|
260
361
|
const normalized = value?.trim().toLowerCase();
|
|
261
362
|
if (normalized === "yesterday") return "yesterday";
|
|
@@ -275,6 +376,8 @@ function buildDigestOptions(
|
|
|
275
376
|
until?: string;
|
|
276
377
|
maxTweets?: string;
|
|
277
378
|
maxLinks?: string;
|
|
379
|
+
liveSync?: boolean;
|
|
380
|
+
liveMode?: string;
|
|
278
381
|
},
|
|
279
382
|
): PeriodDigestOptions | null {
|
|
280
383
|
const maxTweets = parseNonNegativeIntegerOption(
|
|
@@ -291,6 +394,10 @@ function buildDigestOptions(
|
|
|
291
394
|
if (options.maxLinks !== undefined && maxLinks === undefined) {
|
|
292
395
|
return null;
|
|
293
396
|
}
|
|
397
|
+
const liveSyncMode = parseDigestLiveModeOption(options.liveMode);
|
|
398
|
+
if (liveSyncMode === undefined) {
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
294
401
|
return {
|
|
295
402
|
period: parseDigestPeriod(period),
|
|
296
403
|
since: options.since,
|
|
@@ -301,6 +408,8 @@ function buildDigestOptions(
|
|
|
301
408
|
model: options.model,
|
|
302
409
|
maxTweets,
|
|
303
410
|
maxLinks,
|
|
411
|
+
liveSync: options.liveSync !== false,
|
|
412
|
+
liveSyncMode,
|
|
304
413
|
};
|
|
305
414
|
}
|
|
306
415
|
|
|
@@ -323,6 +432,231 @@ function runDigestCli(options: PeriodDigestOptions) {
|
|
|
323
432
|
});
|
|
324
433
|
}
|
|
325
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
|
+
|
|
326
660
|
async function enrichDmItems(
|
|
327
661
|
query: Parameters<typeof listDmConversations>[0],
|
|
328
662
|
options: {
|
|
@@ -426,14 +760,27 @@ program
|
|
|
426
760
|
);
|
|
427
761
|
});
|
|
428
762
|
|
|
429
|
-
program
|
|
430
|
-
.command("auth
|
|
763
|
+
const authCommand = program
|
|
764
|
+
.command("auth")
|
|
765
|
+
.description("Manage live transport");
|
|
766
|
+
|
|
767
|
+
authCommand
|
|
768
|
+
.command("status")
|
|
431
769
|
.description("Show transport status")
|
|
432
770
|
.action(async () => {
|
|
433
771
|
const meta = await getQueryEnvelope();
|
|
434
772
|
print(meta.transport, program.opts().json ?? false);
|
|
435
773
|
});
|
|
436
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
|
+
|
|
437
784
|
program
|
|
438
785
|
.command("archive find")
|
|
439
786
|
.description("Find likely Twitter archives on disk")
|
|
@@ -470,9 +817,13 @@ importCommand
|
|
|
470
817
|
);
|
|
471
818
|
}
|
|
472
819
|
|
|
473
|
-
const
|
|
820
|
+
const asJson = Boolean(program.opts().json);
|
|
821
|
+
const result = await importArchive(resolvedArchivePath, {
|
|
822
|
+
select,
|
|
823
|
+
onProgress: asJson ? undefined : logImportProgress,
|
|
824
|
+
});
|
|
474
825
|
await autoSyncAfterWrite();
|
|
475
|
-
print(result,
|
|
826
|
+
print(result, asJson);
|
|
476
827
|
});
|
|
477
828
|
|
|
478
829
|
importCommand
|
|
@@ -857,6 +1208,60 @@ program
|
|
|
857
1208
|
);
|
|
858
1209
|
});
|
|
859
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
|
+
|
|
860
1265
|
program
|
|
861
1266
|
.command("today")
|
|
862
1267
|
.description("Stream an AI digest of what happened today")
|
|
@@ -864,8 +1269,14 @@ program
|
|
|
864
1269
|
.option("--include-dms", "Include private DM context")
|
|
865
1270
|
.option("--model <model>", "OpenAI model id")
|
|
866
1271
|
.option("--refresh", "Bypass the local digest cache")
|
|
867
|
-
.option("--max-tweets <n>", "Maximum tweet context", "
|
|
1272
|
+
.option("--max-tweets <n>", "Maximum tweet context", "5000")
|
|
868
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
|
+
)
|
|
869
1280
|
.action(async (options) => {
|
|
870
1281
|
await autoUpdateBeforeRead();
|
|
871
1282
|
const digestOptions = buildDigestOptions("today", options);
|
|
@@ -882,8 +1293,14 @@ program
|
|
|
882
1293
|
.option("--until <isoDate>", "End of explicit window")
|
|
883
1294
|
.option("--model <model>", "OpenAI model id")
|
|
884
1295
|
.option("--refresh", "Bypass the local digest cache")
|
|
885
|
-
.option("--max-tweets <n>", "Maximum tweet context", "
|
|
1296
|
+
.option("--max-tweets <n>", "Maximum tweet context", "5000")
|
|
886
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
|
+
)
|
|
887
1304
|
.action(async (period, options) => {
|
|
888
1305
|
await autoUpdateBeforeRead();
|
|
889
1306
|
const digestOptions = buildDigestOptions(period, options);
|
|
@@ -899,7 +1316,7 @@ mentionsCommand
|
|
|
899
1316
|
.command("export [query]")
|
|
900
1317
|
.description("Return mention tweets with plain-text and markdown renderings")
|
|
901
1318
|
.option("--account <accountId>", "Account id")
|
|
902
|
-
.option("--mode <mode>", "birdclaw, xurl, or bird")
|
|
1319
|
+
.option("--mode <mode>", "birdclaw, auto, xurl, or bird")
|
|
903
1320
|
.option("--replied", "Only replied items")
|
|
904
1321
|
.option("--unreplied", "Only unreplied items")
|
|
905
1322
|
.option("--refresh", "Refresh the live xurl cache before returning")
|
|
@@ -919,23 +1336,14 @@ mentionsCommand
|
|
|
919
1336
|
: "all";
|
|
920
1337
|
const limit = Number(options.limit);
|
|
921
1338
|
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({
|
|
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({
|
|
939
1347
|
account: options.account,
|
|
940
1348
|
search: query,
|
|
941
1349
|
replyFilter,
|
|
@@ -982,16 +1390,20 @@ const syncCommand = program
|
|
|
982
1390
|
|
|
983
1391
|
syncCommand
|
|
984
1392
|
.command("timeline")
|
|
985
|
-
.description("Refresh live home timeline through bird")
|
|
1393
|
+
.description("Refresh live home timeline through xurl or bird")
|
|
986
1394
|
.option("--account <accountId>", "Account id")
|
|
1395
|
+
.option("--mode <mode>", "auto, xurl, or bird", "auto")
|
|
987
1396
|
.option("--limit <n>", "Result limit", "100")
|
|
1397
|
+
.option("--max-pages <n>", "Stop after N xurl pages", "3")
|
|
988
1398
|
.option("--for-you", 'Fetch "For You" instead of chronological Following')
|
|
989
1399
|
.option("--cache-ttl <seconds>", "Live-cache freshness window", "120")
|
|
990
1400
|
.option("--refresh", "Bypass live-cache freshness window")
|
|
991
1401
|
.action(async (options) => {
|
|
992
1402
|
const result = await syncHomeTimeline({
|
|
993
1403
|
account: options.account,
|
|
1404
|
+
mode: options.mode,
|
|
994
1405
|
limit: Number(options.limit),
|
|
1406
|
+
maxPages: Number(options.maxPages),
|
|
995
1407
|
following: !options.forYou,
|
|
996
1408
|
refresh: Boolean(options.refresh),
|
|
997
1409
|
cacheTtlMs: Number(options.cacheTtl) * 1000,
|
|
@@ -1004,7 +1416,7 @@ syncCommand
|
|
|
1004
1416
|
.command("mentions")
|
|
1005
1417
|
.description("Refresh live mentions through xurl or bird")
|
|
1006
1418
|
.option("--account <accountId>", "Account id")
|
|
1007
|
-
.option("--mode <mode>", "bird or xurl", "
|
|
1419
|
+
.option("--mode <mode>", "auto, bird, or xurl", "auto")
|
|
1008
1420
|
.option("--limit <n>", "Result limit per page", "20")
|
|
1009
1421
|
.option("--max-pages <n>", "Stop after N pages")
|
|
1010
1422
|
.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;
|