birdclaw 0.2.1 → 0.4.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 +44 -1
- package/README.md +54 -15
- package/package.json +14 -15
- package/playwright.config.ts +5 -2
- package/src/cli.ts +289 -20
- package/src/lib/actions-transport.ts +58 -1
- package/src/lib/archive-import.ts +34 -2
- package/src/lib/backup.ts +271 -33
- package/src/lib/bird-actions.ts +21 -0
- package/src/lib/bird.ts +332 -17
- package/src/lib/blocks.ts +3 -3
- package/src/lib/bookmark-sync-job.ts +3 -3
- package/src/lib/config.ts +34 -1
- package/src/lib/db.ts +321 -14
- package/src/lib/dms-live.ts +3 -3
- package/src/lib/identity-search-index.ts +369 -0
- package/src/lib/mention-threads-live.ts +267 -0
- package/src/lib/mentions-live.ts +18 -7
- package/src/lib/moderation-target.ts +7 -5
- package/src/lib/moderation-write.ts +3 -3
- package/src/lib/profile-affiliation-hydration.ts +189 -0
- package/src/lib/profile-affiliations.ts +262 -0
- package/src/lib/profile-bio-entities.ts +318 -0
- package/src/lib/profile-history.ts +164 -0
- package/src/lib/profile-hydration.ts +8 -24
- package/src/lib/profile-resolver.ts +428 -0
- package/src/lib/queries.ts +522 -68
- package/src/lib/research.ts +607 -0
- package/src/lib/seed.ts +10 -4
- package/src/lib/sqlite.ts +143 -0
- package/src/lib/timeline-collections-live.ts +22 -8
- package/src/lib/timeline-live.ts +182 -0
- package/src/lib/tweet-account-edges.ts +39 -0
- package/src/lib/tweet-lookup.ts +35 -0
- package/src/lib/types.ts +103 -1
- package/src/lib/url-expansion.ts +147 -0
- package/src/lib/whois.ts +1060 -0
- package/src/lib/x-profile.ts +174 -17
- package/src/lib/xurl.ts +41 -6
package/src/lib/bird.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
2
5
|
import { promisify } from "node:util";
|
|
3
6
|
import { getBirdCommand } from "./config";
|
|
4
7
|
import type {
|
|
5
8
|
XurlMentionData,
|
|
6
9
|
XurlMentionsResponse,
|
|
7
10
|
XurlMentionUser,
|
|
11
|
+
XurlReferencedTweet,
|
|
12
|
+
XurlTweetsResponse,
|
|
8
13
|
} from "./types";
|
|
9
14
|
|
|
10
15
|
const execFileAsync = promisify(execFile);
|
|
@@ -28,7 +33,9 @@ interface BirdTweetItem {
|
|
|
28
33
|
retweetCount?: number;
|
|
29
34
|
likeCount?: number;
|
|
30
35
|
conversationId?: string;
|
|
31
|
-
inReplyToStatusId?: string;
|
|
36
|
+
inReplyToStatusId?: string | null;
|
|
37
|
+
quotedStatusId?: string | null;
|
|
38
|
+
quotedTweet?: { id?: string | null } | null;
|
|
32
39
|
author?: BirdTweetAuthor;
|
|
33
40
|
authorId?: string;
|
|
34
41
|
media?: BirdTweetMedia[];
|
|
@@ -66,6 +73,31 @@ export interface BirdDmsResponse {
|
|
|
66
73
|
events: BirdDmEvent[];
|
|
67
74
|
}
|
|
68
75
|
|
|
76
|
+
interface BirdUserOverviewPayload {
|
|
77
|
+
user?: {
|
|
78
|
+
id?: string;
|
|
79
|
+
username?: string;
|
|
80
|
+
name?: string;
|
|
81
|
+
description?: string;
|
|
82
|
+
location?: string;
|
|
83
|
+
url?: string;
|
|
84
|
+
verified?: boolean;
|
|
85
|
+
verifiedType?: string;
|
|
86
|
+
verified_type?: string;
|
|
87
|
+
followersCount?: number;
|
|
88
|
+
followingCount?: number;
|
|
89
|
+
profileImageUrl?: string;
|
|
90
|
+
createdAt?: string;
|
|
91
|
+
entities?: Record<string, unknown>;
|
|
92
|
+
affiliation?: Record<string, unknown>;
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface BirdProfilesPayload {
|
|
97
|
+
users?: NonNullable<BirdUserOverviewPayload["user"]>[];
|
|
98
|
+
errors?: Array<{ target?: string; error?: string }>;
|
|
99
|
+
}
|
|
100
|
+
|
|
69
101
|
function toIsoTimestamp(value: string) {
|
|
70
102
|
const parsed = new Date(value);
|
|
71
103
|
if (Number.isNaN(parsed.getTime())) {
|
|
@@ -140,6 +172,51 @@ function parseBirdJson(stdout: string) {
|
|
|
140
172
|
}
|
|
141
173
|
}
|
|
142
174
|
|
|
175
|
+
function formatBirdCommandError(error: unknown, birdCommand: string) {
|
|
176
|
+
if (
|
|
177
|
+
error instanceof Error &&
|
|
178
|
+
"code" in error &&
|
|
179
|
+
(error as { code?: unknown }).code === "ENOENT"
|
|
180
|
+
) {
|
|
181
|
+
return new Error(
|
|
182
|
+
`bird CLI not found at ${birdCommand}. Install @steipete/bird or set BIRDCLAW_BIRD_COMMAND / mentions.birdCommand to a valid bird binary.`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return error;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function isUnsupportedBirdOptionError(error: unknown, option: string) {
|
|
190
|
+
if (!error || typeof error !== "object") {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
const text = [
|
|
194
|
+
error instanceof Error ? error.message : "",
|
|
195
|
+
"stderr" in error && typeof error.stderr === "string" ? error.stderr : "",
|
|
196
|
+
"stdout" in error && typeof error.stdout === "string" ? error.stdout : "",
|
|
197
|
+
].join("\n");
|
|
198
|
+
return text.includes(option) && /unknown option|error:/i.test(text);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function runBirdJsonCommand(args: string[], timeoutMs?: number) {
|
|
202
|
+
const tempDir = mkdtempSync(join(tmpdir(), "birdclaw-bird-"));
|
|
203
|
+
const stdoutPath = join(tempDir, "stdout.json");
|
|
204
|
+
const birdCommand = getBirdCommand();
|
|
205
|
+
try {
|
|
206
|
+
const shellScript = 'out="$1"; shift; exec "$@" > "$out"';
|
|
207
|
+
await execFileAsync(
|
|
208
|
+
"/bin/bash",
|
|
209
|
+
["-lc", shellScript, "birdclaw-bird", stdoutPath, birdCommand, ...args],
|
|
210
|
+
{ maxBuffer: BIRD_JSON_MAX_BUFFER_BYTES, timeout: timeoutMs },
|
|
211
|
+
);
|
|
212
|
+
return readFileSync(stdoutPath, "utf8");
|
|
213
|
+
} catch (error) {
|
|
214
|
+
throw formatBirdCommandError(error, birdCommand);
|
|
215
|
+
} finally {
|
|
216
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
143
220
|
function getBirdTweetItems(payload: unknown, command: string) {
|
|
144
221
|
if (Array.isArray(payload)) {
|
|
145
222
|
return payload as BirdTweetItem[];
|
|
@@ -156,6 +233,17 @@ function getBirdTweetItems(payload: unknown, command: string) {
|
|
|
156
233
|
throw new Error(`bird ${command} returned unexpected JSON`);
|
|
157
234
|
}
|
|
158
235
|
|
|
236
|
+
function getBirdTweetItem(payload: unknown, command: string) {
|
|
237
|
+
if (payload && typeof payload === "object") {
|
|
238
|
+
const record = payload as { id?: unknown };
|
|
239
|
+
if (typeof record.id === "string" && record.id.length > 0) {
|
|
240
|
+
return payload as BirdTweetItem;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
throw new Error(`bird ${command} returned unexpected JSON`);
|
|
245
|
+
}
|
|
246
|
+
|
|
159
247
|
function toMediaEntities(media: BirdTweetMedia[] | undefined) {
|
|
160
248
|
if (!Array.isArray(media) || media.length === 0) {
|
|
161
249
|
return undefined;
|
|
@@ -175,6 +263,25 @@ function toMediaEntities(media: BirdTweetMedia[] | undefined) {
|
|
|
175
263
|
};
|
|
176
264
|
}
|
|
177
265
|
|
|
266
|
+
function toReferencedTweets(item: BirdTweetItem) {
|
|
267
|
+
const references: XurlReferencedTweet[] = [];
|
|
268
|
+
if (typeof item.inReplyToStatusId === "string" && item.inReplyToStatusId) {
|
|
269
|
+
references.push({ type: "replied_to", id: item.inReplyToStatusId });
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const quotedTweetId =
|
|
273
|
+
typeof item.quotedStatusId === "string" && item.quotedStatusId
|
|
274
|
+
? item.quotedStatusId
|
|
275
|
+
: typeof item.quotedTweet?.id === "string" && item.quotedTweet.id
|
|
276
|
+
? item.quotedTweet.id
|
|
277
|
+
: null;
|
|
278
|
+
if (quotedTweetId) {
|
|
279
|
+
references.push({ type: "quoted", id: quotedTweetId });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return references.length > 0 ? references : undefined;
|
|
283
|
+
}
|
|
284
|
+
|
|
178
285
|
function normalizeBirdTweets(items: BirdTweetItem[]): XurlMentionsResponse {
|
|
179
286
|
const users = new Map<string, XurlMentionUser>();
|
|
180
287
|
const data = items.map((item): XurlMentionData => {
|
|
@@ -196,6 +303,7 @@ function normalizeBirdTweets(items: BirdTweetItem[]): XurlMentionsResponse {
|
|
|
196
303
|
created_at: toIsoTimestamp(item.createdAt),
|
|
197
304
|
conversation_id: item.conversationId ?? item.id,
|
|
198
305
|
entities: toMediaEntities(item.media),
|
|
306
|
+
referenced_tweets: toReferencedTweets(item),
|
|
199
307
|
public_metrics: {
|
|
200
308
|
reply_count: Number(item.replyCount ?? 0),
|
|
201
309
|
retweet_count: Number(item.retweetCount ?? 0),
|
|
@@ -224,12 +332,12 @@ export async function listMentionsViaBird({
|
|
|
224
332
|
}: {
|
|
225
333
|
maxResults: number;
|
|
226
334
|
}): Promise<XurlMentionsResponse> {
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
);
|
|
335
|
+
const stdout = await runBirdJsonCommand([
|
|
336
|
+
"mentions",
|
|
337
|
+
"-n",
|
|
338
|
+
String(maxResults),
|
|
339
|
+
"--json",
|
|
340
|
+
]);
|
|
233
341
|
const payload = parseBirdJson(stdout);
|
|
234
342
|
|
|
235
343
|
return normalizeBirdTweets(getBirdTweetItems(payload, "mentions"));
|
|
@@ -246,7 +354,6 @@ async function listTweetsViaBirdCommand({
|
|
|
246
354
|
all?: boolean;
|
|
247
355
|
maxPages?: number;
|
|
248
356
|
}): Promise<XurlMentionsResponse> {
|
|
249
|
-
const birdCommand = getBirdCommand();
|
|
250
357
|
const args = [command, "-n", String(maxResults), "--json"];
|
|
251
358
|
if (all) {
|
|
252
359
|
args.push("--all");
|
|
@@ -254,9 +361,7 @@ async function listTweetsViaBirdCommand({
|
|
|
254
361
|
if (maxPages !== undefined) {
|
|
255
362
|
args.push("--max-pages", String(maxPages));
|
|
256
363
|
}
|
|
257
|
-
const
|
|
258
|
-
maxBuffer: BIRD_JSON_MAX_BUFFER_BYTES,
|
|
259
|
-
});
|
|
364
|
+
const stdout = await runBirdJsonCommand(args);
|
|
260
365
|
const payload = parseBirdJson(stdout);
|
|
261
366
|
|
|
262
367
|
return normalizeBirdTweets(getBirdTweetItems(payload, command));
|
|
@@ -296,17 +401,75 @@ export async function listBookmarkedTweetsViaBird({
|
|
|
296
401
|
});
|
|
297
402
|
}
|
|
298
403
|
|
|
404
|
+
export async function lookupTweetsByIdsViaBird(
|
|
405
|
+
ids: string[],
|
|
406
|
+
): Promise<XurlTweetsResponse> {
|
|
407
|
+
if (ids.length === 0) {
|
|
408
|
+
return { data: [] };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const tweets = await Promise.all(
|
|
412
|
+
ids.map(async (id) => {
|
|
413
|
+
const stdout = await runBirdJsonCommand(["read", id, "--json"]);
|
|
414
|
+
return getBirdTweetItem(parseBirdJson(stdout), "read");
|
|
415
|
+
}),
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
return normalizeBirdTweets(tweets);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export async function listHomeTimelineViaBird({
|
|
422
|
+
maxResults,
|
|
423
|
+
following = true,
|
|
424
|
+
}: {
|
|
425
|
+
maxResults: number;
|
|
426
|
+
following?: boolean;
|
|
427
|
+
}): Promise<XurlMentionsResponse> {
|
|
428
|
+
const args = ["home", "-n", String(maxResults), "--json"];
|
|
429
|
+
if (following) {
|
|
430
|
+
args.push("--following");
|
|
431
|
+
}
|
|
432
|
+
const stdout = await runBirdJsonCommand(args);
|
|
433
|
+
const payload = parseBirdJson(stdout);
|
|
434
|
+
|
|
435
|
+
return normalizeBirdTweets(getBirdTweetItems(payload, "home"));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export async function listThreadViaBird({
|
|
439
|
+
tweetId,
|
|
440
|
+
all,
|
|
441
|
+
maxPages,
|
|
442
|
+
timeoutMs,
|
|
443
|
+
}: {
|
|
444
|
+
tweetId: string;
|
|
445
|
+
all?: boolean;
|
|
446
|
+
maxPages?: number;
|
|
447
|
+
timeoutMs?: number;
|
|
448
|
+
}): Promise<XurlMentionsResponse> {
|
|
449
|
+
const args = ["thread", tweetId, "--json"];
|
|
450
|
+
if (all) {
|
|
451
|
+
args.push("--all");
|
|
452
|
+
}
|
|
453
|
+
if (maxPages !== undefined) {
|
|
454
|
+
args.push("--max-pages", String(maxPages));
|
|
455
|
+
}
|
|
456
|
+
const stdout = await runBirdJsonCommand(args, timeoutMs);
|
|
457
|
+
const payload = parseBirdJson(stdout);
|
|
458
|
+
|
|
459
|
+
return normalizeBirdTweets(getBirdTweetItems(payload, "thread"));
|
|
460
|
+
}
|
|
461
|
+
|
|
299
462
|
export async function listDirectMessagesViaBird({
|
|
300
463
|
maxResults,
|
|
301
464
|
}: {
|
|
302
465
|
maxResults: number;
|
|
303
466
|
}): Promise<BirdDmsResponse> {
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
);
|
|
467
|
+
const stdout = await runBirdJsonCommand([
|
|
468
|
+
"dms",
|
|
469
|
+
"-n",
|
|
470
|
+
String(maxResults),
|
|
471
|
+
"--json",
|
|
472
|
+
]);
|
|
310
473
|
const payload = parseBirdJson(stdout);
|
|
311
474
|
if (
|
|
312
475
|
!payload ||
|
|
@@ -320,3 +483,155 @@ export async function listDirectMessagesViaBird({
|
|
|
320
483
|
|
|
321
484
|
return payload as BirdDmsResponse;
|
|
322
485
|
}
|
|
486
|
+
|
|
487
|
+
export async function lookupProfileViaBird(
|
|
488
|
+
usernameOrId: string,
|
|
489
|
+
): Promise<XurlMentionUser | null> {
|
|
490
|
+
const target = usernameOrId.trim().replace(/^@/, "");
|
|
491
|
+
if (!target) {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
let stdout: string;
|
|
496
|
+
try {
|
|
497
|
+
stdout = await runBirdJsonCommand([
|
|
498
|
+
"user",
|
|
499
|
+
target,
|
|
500
|
+
"--json",
|
|
501
|
+
"--profile-only",
|
|
502
|
+
]);
|
|
503
|
+
} catch (error) {
|
|
504
|
+
if (!isUnsupportedBirdOptionError(error, "--profile-only")) {
|
|
505
|
+
throw error;
|
|
506
|
+
}
|
|
507
|
+
stdout = await runBirdJsonCommand([
|
|
508
|
+
"user",
|
|
509
|
+
target,
|
|
510
|
+
"--json",
|
|
511
|
+
"--count",
|
|
512
|
+
"1",
|
|
513
|
+
]);
|
|
514
|
+
}
|
|
515
|
+
const payload = parseBirdJson(stdout) as BirdUserOverviewPayload;
|
|
516
|
+
return toXurlMentionUser(payload.user);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function toXurlMentionUser(
|
|
520
|
+
user: BirdUserOverviewPayload["user"],
|
|
521
|
+
): XurlMentionUser | null {
|
|
522
|
+
if (!user?.id || !user.username) {
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return {
|
|
527
|
+
id: String(user.id),
|
|
528
|
+
username: String(user.username).replace(/^@/, ""),
|
|
529
|
+
name: String(user.name ?? user.username),
|
|
530
|
+
description:
|
|
531
|
+
typeof user.description === "string" ? user.description : undefined,
|
|
532
|
+
location: typeof user.location === "string" ? user.location : undefined,
|
|
533
|
+
url: typeof user.url === "string" ? user.url : undefined,
|
|
534
|
+
verified: typeof user.verified === "boolean" ? user.verified : undefined,
|
|
535
|
+
verified_type:
|
|
536
|
+
typeof user.verifiedType === "string"
|
|
537
|
+
? user.verifiedType
|
|
538
|
+
: typeof user.verified_type === "string"
|
|
539
|
+
? user.verified_type
|
|
540
|
+
: undefined,
|
|
541
|
+
profile_image_url:
|
|
542
|
+
typeof user.profileImageUrl === "string"
|
|
543
|
+
? user.profileImageUrl
|
|
544
|
+
: undefined,
|
|
545
|
+
entities:
|
|
546
|
+
user.entities && typeof user.entities === "object"
|
|
547
|
+
? user.entities
|
|
548
|
+
: undefined,
|
|
549
|
+
affiliation:
|
|
550
|
+
user.affiliation && typeof user.affiliation === "object"
|
|
551
|
+
? user.affiliation
|
|
552
|
+
: undefined,
|
|
553
|
+
created_at: typeof user.createdAt === "string" ? user.createdAt : undefined,
|
|
554
|
+
public_metrics: {
|
|
555
|
+
followers_count: Number(user.followersCount ?? 0),
|
|
556
|
+
following_count: Number(user.followingCount ?? 0),
|
|
557
|
+
},
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
export async function lookupProfilesViaBird(
|
|
562
|
+
usernameOrIds: string[],
|
|
563
|
+
): Promise<
|
|
564
|
+
Array<{ target: string; user: XurlMentionUser | null; error?: string }>
|
|
565
|
+
> {
|
|
566
|
+
const targets = Array.from(
|
|
567
|
+
new Set(
|
|
568
|
+
usernameOrIds
|
|
569
|
+
.map((target) => target.trim().replace(/^@/, ""))
|
|
570
|
+
.filter((target) => target.length > 0),
|
|
571
|
+
),
|
|
572
|
+
);
|
|
573
|
+
if (targets.length === 0) {
|
|
574
|
+
return [];
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
try {
|
|
578
|
+
const stdout = await runBirdJsonCommand(["profiles", ...targets, "--json"]);
|
|
579
|
+
const payload = parseBirdJson(stdout) as BirdProfilesPayload;
|
|
580
|
+
const users = (payload.users ?? [])
|
|
581
|
+
.map(toXurlMentionUser)
|
|
582
|
+
.filter((user): user is XurlMentionUser => Boolean(user));
|
|
583
|
+
const byTarget = new Map<string, XurlMentionUser>();
|
|
584
|
+
for (const user of users) {
|
|
585
|
+
byTarget.set(String(user.id), user);
|
|
586
|
+
byTarget.set(user.username.toLowerCase(), user);
|
|
587
|
+
}
|
|
588
|
+
const errors = new Map(
|
|
589
|
+
(payload.errors ?? []).map((item) => [
|
|
590
|
+
String(item.target ?? "")
|
|
591
|
+
.replace(/^@/, "")
|
|
592
|
+
.toLowerCase(),
|
|
593
|
+
item.error ?? "Unknown error",
|
|
594
|
+
]),
|
|
595
|
+
);
|
|
596
|
+
return targets.map((target) => ({
|
|
597
|
+
target,
|
|
598
|
+
user: byTarget.get(target.toLowerCase()) ?? null,
|
|
599
|
+
...(errors.has(target.toLowerCase())
|
|
600
|
+
? { error: errors.get(target.toLowerCase()) }
|
|
601
|
+
: {}),
|
|
602
|
+
}));
|
|
603
|
+
} catch (error) {
|
|
604
|
+
if (!isUnsupportedBirdOptionError(error, "profiles")) {
|
|
605
|
+
throw error;
|
|
606
|
+
}
|
|
607
|
+
return Promise.all(
|
|
608
|
+
targets.map(async (target) => {
|
|
609
|
+
try {
|
|
610
|
+
return { target, user: await lookupProfileViaBird(target) };
|
|
611
|
+
} catch (lookupError) {
|
|
612
|
+
return {
|
|
613
|
+
target,
|
|
614
|
+
user: null,
|
|
615
|
+
error:
|
|
616
|
+
lookupError instanceof Error
|
|
617
|
+
? lookupError.message
|
|
618
|
+
: String(lookupError),
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
}),
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
export const __test__ = {
|
|
627
|
+
toIsoTimestamp,
|
|
628
|
+
escapeJsonStringControlChars,
|
|
629
|
+
parseBirdJson,
|
|
630
|
+
formatBirdCommandError,
|
|
631
|
+
isUnsupportedBirdOptionError,
|
|
632
|
+
getBirdTweetItems,
|
|
633
|
+
getBirdTweetItem,
|
|
634
|
+
toMediaEntities,
|
|
635
|
+
toReferencedTweets,
|
|
636
|
+
normalizeBirdTweets,
|
|
637
|
+
};
|
package/src/lib/blocks.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type Database from "
|
|
1
|
+
import type { Database } from "./sqlite";
|
|
2
2
|
import { getNativeDb } from "./db";
|
|
3
3
|
import {
|
|
4
4
|
getAccountHandle,
|
|
@@ -16,7 +16,7 @@ function remoteBlockSyncDisabled() {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
function upsertRemoteBlock(
|
|
19
|
-
db: Database
|
|
19
|
+
db: Database,
|
|
20
20
|
accountId: string,
|
|
21
21
|
profileId: string,
|
|
22
22
|
blockedAt: string,
|
|
@@ -33,7 +33,7 @@ function upsertRemoteBlock(
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
function pruneRemoteBlocks(
|
|
36
|
-
db: Database
|
|
36
|
+
db: Database,
|
|
37
37
|
accountId: string,
|
|
38
38
|
profileIds: string[],
|
|
39
39
|
) {
|
|
@@ -3,7 +3,7 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
|
-
import type Database from "
|
|
6
|
+
import type { Database } from "./sqlite";
|
|
7
7
|
import { maybeAutoSyncBackup, type BackupAutoUpdateResult } from "./backup";
|
|
8
8
|
import { ensureBirdclawDirs, getBirdclawPaths } from "./config";
|
|
9
9
|
import { getNativeDb } from "./db";
|
|
@@ -31,7 +31,7 @@ export interface BookmarkSyncJobOptions {
|
|
|
31
31
|
cacheTtlMs?: number;
|
|
32
32
|
logPath?: string;
|
|
33
33
|
lockPath?: string;
|
|
34
|
-
db?: Database
|
|
34
|
+
db?: Database;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export interface BookmarkSyncAuditEntry {
|
|
@@ -117,7 +117,7 @@ export function getDefaultBookmarkSyncLockPath() {
|
|
|
117
117
|
return path.join(getBirdclawPaths().rootDir, "locks", "bookmarks-sync.lock");
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
function countBookmarks(db: Database
|
|
120
|
+
function countBookmarks(db: Database) {
|
|
121
121
|
const row = db
|
|
122
122
|
.prepare("select count(*) as count from tweet_collections where kind = ?")
|
|
123
123
|
.get("bookmarks") as { count: number };
|
package/src/lib/config.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
accessSync,
|
|
3
|
+
constants,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
} from "node:fs";
|
|
2
8
|
import os from "node:os";
|
|
3
9
|
import path from "node:path";
|
|
4
10
|
|
|
@@ -128,6 +134,28 @@ export function resolveActionsTransport(
|
|
|
128
134
|
return "auto";
|
|
129
135
|
}
|
|
130
136
|
|
|
137
|
+
function findCommandOnPath(command: string) {
|
|
138
|
+
const pathValue = process.env.PATH;
|
|
139
|
+
if (!pathValue) {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for (const directory of pathValue.split(path.delimiter)) {
|
|
144
|
+
if (!directory) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const candidate = path.join(directory, command);
|
|
148
|
+
try {
|
|
149
|
+
accessSync(candidate, constants.X_OK);
|
|
150
|
+
return candidate;
|
|
151
|
+
} catch {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
131
159
|
export function getBirdCommand() {
|
|
132
160
|
const envCommand = process.env.BIRDCLAW_BIRD_COMMAND?.trim();
|
|
133
161
|
if (envCommand) {
|
|
@@ -139,6 +167,11 @@ export function getBirdCommand() {
|
|
|
139
167
|
return configuredCommand;
|
|
140
168
|
}
|
|
141
169
|
|
|
170
|
+
const pathCommand = findCommandOnPath("bird");
|
|
171
|
+
if (pathCommand) {
|
|
172
|
+
return pathCommand;
|
|
173
|
+
}
|
|
174
|
+
|
|
142
175
|
return path.join(os.homedir(), "Projects", "bird", "bird");
|
|
143
176
|
}
|
|
144
177
|
|