birdclaw 0.8.2 → 0.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +10 -8
- package/bin/birdclaw.mjs +4 -66
- package/dist/cli/birdclaw.js +28165 -0
- package/dist/client/assets/AvatarChip-DLoqM5xD.js +1 -0
- package/dist/client/assets/ProfilePreview-BejAuYsJ.js +1 -0
- package/dist/client/assets/SavedTimelineView-BIS7HKpp.js +1 -0
- package/dist/client/assets/SmartTimestamp-BR0-9se2.js +1 -0
- package/dist/client/assets/TimelineRouteFrame-CyGPj0eM.js +1 -0
- package/dist/client/assets/blocks-7UDKQp8H.js +1 -0
- package/dist/client/assets/bookmarks-CbBkMvXk.js +1 -0
- package/dist/client/assets/chunk-C0phanGV.js +1 -0
- package/dist/client/assets/circle-check-C_Vq3ElD.js +1 -0
- package/dist/client/assets/client-stream-contracts-ys3XhWG4.js +5 -0
- package/dist/client/assets/createLucideIcon-JV8isFFR.js +1 -0
- package/dist/client/assets/data-sources-Cm2bpNvg.js +1 -0
- package/dist/client/assets/discuss-DTjaOugw.js +1 -0
- package/dist/client/assets/dms-CGd6Eogq.js +1 -0
- package/dist/client/assets/inbox-BHlGuOy2.js +1 -0
- package/dist/client/assets/index-CEIG7S6s.js +31 -0
- package/dist/client/assets/index-bpFrQ5yG.css +1 -0
- package/dist/client/assets/likes-cLrcAQem.js +1 -0
- package/dist/client/assets/links-CM9QO1LC.js +1 -0
- package/dist/client/assets/mapbox-BxTlqXt9.js +2 -0
- package/dist/client/assets/mapbox-gl-dgf_hX3a.js +3215 -0
- package/dist/client/assets/mentions-CxKCgzIf.js +1 -0
- package/dist/client/assets/network-map-B5a3whqj.js +2 -0
- package/dist/client/assets/profile-analyze-CRtNEwlb.js +1 -0
- package/dist/client/assets/profile-hydration-client-YEtJsNTv.js +1 -0
- package/dist/client/assets/profiles._handle-C82_HoKX.js +1 -0
- package/dist/client/assets/query-client-fYu2rzRC.js +17 -0
- package/dist/client/assets/rate-limits-ScaEwdVK.js +1 -0
- package/dist/client/assets/react-dom-DHl6Ug7F.js +1 -0
- package/dist/client/assets/react-nBylq1U5.js +1 -0
- package/dist/client/assets/repeat-2-xCwHM9hD.js +1 -0
- package/dist/client/assets/routes-q7ZaW4En.js +1 -0
- package/dist/client/assets/schemas-I9r6F6ya.js +64 -0
- package/dist/client/assets/search-Dq5kYsxA.js +1 -0
- package/dist/client/assets/sparkles-CQVlvHsZ.js +1 -0
- package/dist/client/assets/styles-B8s3tt3r.css +2 -0
- package/dist/client/assets/today-CeG1O7Py.js +1 -0
- package/dist/client/assets/triangle-alert-CBL7s2pf.js +1 -0
- package/dist/client/assets/useTimelineRouteData-1HsqgvUi.js +1 -0
- package/dist/client/assets/users-CHbKlqUr.js +1 -0
- package/dist/server/assets/AvatarChip-zUDAEmjL.js +105 -0
- package/dist/server/assets/FeedState-ntW0Q2GB.js +214 -0
- package/dist/server/assets/ProfileAnalysisStream-BFvhVdhn.js +198 -0
- package/dist/server/assets/ProfilePreview-C6gAor_j.js +752 -0
- package/dist/server/assets/SavedTimelineView-BG1jnBp1.js +64 -0
- package/dist/server/assets/SmartTimestamp-DfwE1NGf.js +45 -0
- package/dist/server/assets/SyncNowButton-BOn_IBXS.js +76 -0
- package/dist/server/assets/TimelineRouteFrame-CU7ilM7U.js +85 -0
- package/dist/server/assets/TweetRichText-DhCVv1-6.js +74 -0
- package/dist/server/assets/_tanstack-start-manifest_v-C-ZRnG_n.js +148 -0
- package/dist/server/assets/account-selection-CTdFGWRk.js +44 -0
- package/dist/server/assets/blocks-C1mgGTB3.js +299 -0
- package/dist/server/assets/blocks-DSgQghqP.js +17 -0
- package/dist/server/assets/bookmarks-BJN8tykY.js +14 -0
- package/dist/server/assets/client-stream-contracts-C2xbWVjm.js +769 -0
- package/dist/server/assets/data-sources-BnA1Q94p.js +180 -0
- package/dist/server/assets/discuss-CJ1dau1j.js +15 -0
- package/dist/server/assets/discuss-QTo08NCN.js +338 -0
- package/dist/server/assets/dms-BuLj4bp8.js +368 -0
- package/dist/server/assets/dms-DAyQrVM2.js +240 -0
- package/dist/server/assets/empty-plugin-adapters-DzrEkdBF.js +5 -0
- package/dist/server/assets/inbox-CkGOaW1q.js +214 -0
- package/dist/server/assets/inbox-DpWd1EzC.js +146 -0
- package/dist/server/assets/likes-DBOAtsJU.js +14 -0
- package/dist/server/assets/links-DAhSjynu.js +240 -0
- package/dist/server/assets/links-DmH5lxHO.js +468 -0
- package/dist/server/assets/mentions-BH2MzKtB.js +26 -0
- package/dist/server/assets/network-map-D3CzRcC4.js +224 -0
- package/dist/server/assets/network-map-D5DoLOwR.js +582 -0
- package/dist/server/assets/profile-analyze-BP65PED0.js +90 -0
- package/dist/server/assets/profile-analyze-C1NoM0tV.js +9 -0
- package/dist/server/assets/profile-hydration-client-GJ4kwJS8.js +133 -0
- package/dist/server/assets/profiles._handle-DjwlN_ac.js +184 -0
- package/dist/server/assets/profiles._handle-DsLNj2Ca.js +12 -0
- package/dist/server/assets/query-client-BdauHBgd.js +923 -0
- package/dist/server/assets/rate-limits-VAr2qzYD.js +262 -0
- package/dist/server/assets/route-search-BUlQ5lGM.js +117 -0
- package/dist/server/assets/router-DqkwSPiM.js +17026 -0
- package/dist/server/assets/routes-A9c9ZhwN.js +26 -0
- package/dist/server/assets/start-FaDOTmto.js +4 -0
- package/dist/server/assets/today-B5hVM57Z.js +17 -0
- package/dist/server/assets/today-fvGiZ7ud.js +303 -0
- package/dist/server/assets/ui-qbmcMSSX.js +137 -0
- package/{src/components/useDebouncedValue.ts → dist/server/assets/useDebouncedValue-DOnA6Xcy.js} +4 -4
- package/dist/server/assets/useTimelineRouteData-27cxynxa.js +1101 -0
- package/dist/server/server.js +5520 -0
- package/package.json +27 -34
- package/playwright.config.ts +0 -35
- package/scripts/browser-perf.mjs +0 -427
- package/scripts/build-docs-site.mjs +0 -967
- package/scripts/docs-site-assets.mjs +0 -311
- package/scripts/run-vitest.mjs +0 -21
- package/scripts/sanitize-node-options.mjs +0 -23
- package/scripts/start-test-server.mjs +0 -42
- package/src/cli-moderation.ts +0 -210
- package/src/cli.ts +0 -2336
- package/src/components/AccountSwitcher.tsx +0 -186
- package/src/components/AppNav.tsx +0 -118
- package/src/components/AvatarChip.tsx +0 -58
- package/src/components/BrandMark.tsx +0 -67
- package/src/components/ConversationThread.tsx +0 -131
- package/src/components/DmWorkspace.tsx +0 -286
- package/src/components/EmbeddedTweetCard.tsx +0 -55
- package/src/components/FeedState.tsx +0 -147
- package/src/components/FloatingPreview.tsx +0 -382
- package/src/components/InboxCard.tsx +0 -152
- package/src/components/LinkPreviewCard.tsx +0 -292
- package/src/components/MarkdownViewer.tsx +0 -777
- package/src/components/ProfileAnalysisStream.tsx +0 -428
- package/src/components/ProfilePreview.tsx +0 -129
- package/src/components/SavedTimelineView.tsx +0 -155
- package/src/components/SmartTimestamp.tsx +0 -72
- package/src/components/SyncNowButton.tsx +0 -140
- package/src/components/ThemeSlider.tsx +0 -80
- package/src/components/TimelineCard.tsx +0 -533
- package/src/components/TimelineRouteFrame.tsx +0 -186
- package/src/components/TweetArticleCard.tsx +0 -66
- package/src/components/TweetMediaGrid.tsx +0 -185
- package/src/components/TweetRichText.tsx +0 -159
- package/src/components/account-selection.ts +0 -64
- package/src/components/useTimelineRouteData.ts +0 -323
- package/src/lib/account-sync-job.ts +0 -666
- package/src/lib/actions-transport.ts +0 -300
- package/src/lib/api-client.ts +0 -419
- package/src/lib/archive-finder.ts +0 -185
- package/src/lib/archive-import.ts +0 -2764
- package/src/lib/authored-live.ts +0 -1072
- package/src/lib/avatar-cache.ts +0 -349
- package/src/lib/backup.ts +0 -2741
- package/src/lib/bird-actions.ts +0 -294
- package/src/lib/bird-command.ts +0 -130
- package/src/lib/bird.ts +0 -1140
- package/src/lib/blocklist.ts +0 -136
- package/src/lib/blocks-write.ts +0 -167
- package/src/lib/blocks.ts +0 -389
- package/src/lib/bookmark-sync-job.ts +0 -536
- package/src/lib/client-cache.ts +0 -109
- package/src/lib/config.ts +0 -224
- package/src/lib/conversation-surface.ts +0 -205
- package/src/lib/data-sources.ts +0 -219
- package/src/lib/db.ts +0 -1135
- package/src/lib/dms-live.ts +0 -933
- package/src/lib/effect-runtime.ts +0 -45
- package/src/lib/follow-graph.ts +0 -1097
- package/src/lib/geocoding.ts +0 -296
- package/src/lib/http-effect.ts +0 -222
- package/src/lib/identity-search-index.ts +0 -369
- package/src/lib/inbox.ts +0 -241
- package/src/lib/link-index.ts +0 -641
- package/src/lib/link-insights.ts +0 -860
- package/src/lib/link-preview-metadata.ts +0 -754
- package/src/lib/location.ts +0 -137
- package/src/lib/media-fetch.ts +0 -860
- package/src/lib/media-includes.ts +0 -165
- package/src/lib/mention-threads-live.ts +0 -917
- package/src/lib/mentions-export.ts +0 -147
- package/src/lib/mentions-live.ts +0 -1238
- package/src/lib/moderation-target.ts +0 -216
- package/src/lib/moderation-write.ts +0 -131
- package/src/lib/mutes-write.ts +0 -167
- package/src/lib/mutes.ts +0 -84
- package/src/lib/ndjson-stream.ts +0 -106
- package/src/lib/network-map.ts +0 -382
- package/src/lib/openai.ts +0 -117
- package/src/lib/period-digest.ts +0 -1552
- package/src/lib/present.ts +0 -118
- package/src/lib/profile-affiliation-hydration.ts +0 -228
- package/src/lib/profile-affiliations.ts +0 -262
- package/src/lib/profile-analysis.ts +0 -1272
- package/src/lib/profile-bio-entities.ts +0 -318
- package/src/lib/profile-history.ts +0 -164
- package/src/lib/profile-hydration.ts +0 -180
- package/src/lib/profile-replies.ts +0 -77
- package/src/lib/profile-resolver.ts +0 -650
- package/src/lib/queries.ts +0 -2381
- package/src/lib/research.ts +0 -652
- package/src/lib/search-discussion.ts +0 -1016
- package/src/lib/seed.ts +0 -613
- package/src/lib/sqlite.ts +0 -147
- package/src/lib/sync-cache.ts +0 -65
- package/src/lib/theme-transition.ts +0 -113
- package/src/lib/theme.tsx +0 -165
- package/src/lib/timeline-collections-live.ts +0 -581
- package/src/lib/timeline-live.ts +0 -452
- package/src/lib/tweet-account-edges.ts +0 -48
- package/src/lib/tweet-lookup.ts +0 -46
- package/src/lib/tweet-render.ts +0 -430
- package/src/lib/tweet-search-live.ts +0 -566
- package/src/lib/types.ts +0 -723
- package/src/lib/ui.ts +0 -529
- package/src/lib/url-expansion-store.ts +0 -110
- package/src/lib/url-expansion.ts +0 -343
- package/src/lib/url-safety.ts +0 -220
- package/src/lib/web-sync.ts +0 -516
- package/src/lib/whois.ts +0 -1079
- package/src/lib/x-profile.ts +0 -365
- package/src/lib/x-web.ts +0 -199
- package/src/lib/xurl-rate-limits.ts +0 -267
- package/src/lib/xurl.ts +0 -1677
- package/src/routeTree.gen.ts +0 -723
- package/src/router.tsx +0 -20
- package/src/routes/__root.tsx +0 -79
- package/src/routes/api/action.tsx +0 -147
- package/src/routes/api/avatar.tsx +0 -50
- package/src/routes/api/blocks.tsx +0 -35
- package/src/routes/api/conversation.tsx +0 -45
- package/src/routes/api/data-sources.tsx +0 -24
- package/src/routes/api/inbox.tsx +0 -43
- package/src/routes/api/link-insights.tsx +0 -85
- package/src/routes/api/link-preview.tsx +0 -47
- package/src/routes/api/network-map.tsx +0 -55
- package/src/routes/api/period-digest.tsx +0 -101
- package/src/routes/api/profile-analysis.tsx +0 -97
- package/src/routes/api/profile-hydrate.tsx +0 -54
- package/src/routes/api/query.tsx +0 -115
- package/src/routes/api/search-discussion.tsx +0 -113
- package/src/routes/api/status.tsx +0 -28
- package/src/routes/api/sync.tsx +0 -105
- package/src/routes/api/xurl-rate-limits.tsx +0 -24
- package/src/routes/blocks.tsx +0 -377
- package/src/routes/bookmarks.tsx +0 -18
- package/src/routes/data-sources.tsx +0 -257
- package/src/routes/discuss.tsx +0 -419
- package/src/routes/dms.tsx +0 -432
- package/src/routes/inbox.tsx +0 -226
- package/src/routes/index.tsx +0 -32
- package/src/routes/likes.tsx +0 -18
- package/src/routes/links.tsx +0 -994
- package/src/routes/mentions.tsx +0 -32
- package/src/routes/network-map.tsx +0 -1035
- package/src/routes/profile-analyze.tsx +0 -112
- package/src/routes/profiles.$handle.tsx +0 -228
- package/src/routes/rate-limits.tsx +0 -309
- package/src/routes/today.tsx +0 -511
- package/src/styles.css +0 -257
- package/tsconfig.json +0 -29
- package/vite.config.ts +0 -29
- /package/{public → dist/client}/birdclaw-mark.png +0 -0
- /package/{public → dist/client}/favicon.ico +0 -0
- /package/{public → dist/client}/logo192.png +0 -0
- /package/{public → dist/client}/logo512.png +0 -0
- /package/{public → dist/client}/manifest.json +0 -0
- /package/{public → dist/client}/robots.txt +0 -0
|
@@ -1,666 +0,0 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
|
-
import fs from "node:fs/promises";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { promisify } from "node:util";
|
|
6
|
-
import { maybeAutoSyncBackup, type BackupAutoUpdateResult } from "./backup";
|
|
7
|
-
import { ensureBirdclawDirs, getBirdclawPaths } from "./config";
|
|
8
|
-
import { getNativeDb } from "./db";
|
|
9
|
-
import { syncDirectMessagesViaCachedBird } from "./dms-live";
|
|
10
|
-
import { syncMentionThreads } from "./mention-threads-live";
|
|
11
|
-
import { syncMentions } from "./mentions-live";
|
|
12
|
-
import type { Database } from "./sqlite";
|
|
13
|
-
import {
|
|
14
|
-
syncTimelineCollection,
|
|
15
|
-
type TimelineCollectionKind,
|
|
16
|
-
type TimelineCollectionMode,
|
|
17
|
-
} from "./timeline-collections-live";
|
|
18
|
-
import { syncHomeTimeline } from "./timeline-live";
|
|
19
|
-
|
|
20
|
-
const execFileAsync = promisify(execFile);
|
|
21
|
-
const DEFAULT_ACCOUNT_SYNC_INTERVAL_SECONDS = 30 * 60;
|
|
22
|
-
const DEFAULT_ACCOUNT_SYNC_LIMIT = 100;
|
|
23
|
-
const DEFAULT_ACCOUNT_SYNC_MAX_PAGES = 3;
|
|
24
|
-
const DEFAULT_ACCOUNT_SYNC_LABEL = "com.steipete.birdclaw.account-sync";
|
|
25
|
-
const DEFAULT_LAUNCHD_PATH =
|
|
26
|
-
"/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
|
|
27
|
-
const DEFAULT_LOCK_STALE_MS = 60 * 60 * 1000;
|
|
28
|
-
|
|
29
|
-
export type AccountSyncStepKind =
|
|
30
|
-
| "timeline"
|
|
31
|
-
| "mentions"
|
|
32
|
-
| "mention-threads"
|
|
33
|
-
| "likes"
|
|
34
|
-
| "bookmarks"
|
|
35
|
-
| "dms";
|
|
36
|
-
|
|
37
|
-
export interface AccountSyncJobOptions {
|
|
38
|
-
account?: string;
|
|
39
|
-
steps?: AccountSyncStepKind[];
|
|
40
|
-
mode?: TimelineCollectionMode;
|
|
41
|
-
limit?: number;
|
|
42
|
-
maxPages?: number;
|
|
43
|
-
refresh?: boolean;
|
|
44
|
-
cacheTtlMs?: number;
|
|
45
|
-
allowBirdAccount?: boolean;
|
|
46
|
-
logPath?: string;
|
|
47
|
-
lockPath?: string;
|
|
48
|
-
db?: Database;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface AccountSyncAuditStep {
|
|
52
|
-
kind: AccountSyncStepKind;
|
|
53
|
-
ok: boolean;
|
|
54
|
-
count: number;
|
|
55
|
-
source?: string;
|
|
56
|
-
error?: string;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export interface AccountSyncAuditEntry {
|
|
60
|
-
job: "account-sync";
|
|
61
|
-
ok: boolean;
|
|
62
|
-
startedAt: string;
|
|
63
|
-
finishedAt: string;
|
|
64
|
-
durationMs: number;
|
|
65
|
-
host: string;
|
|
66
|
-
pid: number;
|
|
67
|
-
options: {
|
|
68
|
-
account?: string;
|
|
69
|
-
steps: AccountSyncStepKind[];
|
|
70
|
-
mode: TimelineCollectionMode;
|
|
71
|
-
limit: number;
|
|
72
|
-
maxPages: number;
|
|
73
|
-
refresh: boolean;
|
|
74
|
-
cacheTtlMs?: number;
|
|
75
|
-
allowBirdAccount?: boolean;
|
|
76
|
-
};
|
|
77
|
-
steps: AccountSyncAuditStep[];
|
|
78
|
-
skipped?: "already-running";
|
|
79
|
-
backup?: BackupAutoUpdateResult;
|
|
80
|
-
error?: string;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export interface AccountSyncLaunchAgentOptions {
|
|
84
|
-
label?: string;
|
|
85
|
-
intervalSeconds?: number;
|
|
86
|
-
program?: string;
|
|
87
|
-
account?: string;
|
|
88
|
-
steps?: AccountSyncStepKind[];
|
|
89
|
-
mode?: TimelineCollectionMode;
|
|
90
|
-
limit?: number;
|
|
91
|
-
maxPages?: number;
|
|
92
|
-
refresh?: boolean;
|
|
93
|
-
allowBirdAccount?: boolean;
|
|
94
|
-
cacheTtlSeconds?: number;
|
|
95
|
-
logPath?: string;
|
|
96
|
-
envFile?: string;
|
|
97
|
-
stdoutPath?: string;
|
|
98
|
-
stderrPath?: string;
|
|
99
|
-
launchAgentsDir?: string;
|
|
100
|
-
load?: boolean;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export interface AccountSyncLaunchAgentInstallResult {
|
|
104
|
-
ok: true;
|
|
105
|
-
label: string;
|
|
106
|
-
plistPath: string;
|
|
107
|
-
loaded: boolean;
|
|
108
|
-
programArguments: string[];
|
|
109
|
-
logPath: string;
|
|
110
|
-
stdoutPath: string;
|
|
111
|
-
stderrPath: string;
|
|
112
|
-
intervalSeconds: number;
|
|
113
|
-
envFile?: string;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const DEFAULT_STEPS: AccountSyncStepKind[] = [
|
|
117
|
-
"timeline",
|
|
118
|
-
"mentions",
|
|
119
|
-
"mention-threads",
|
|
120
|
-
"likes",
|
|
121
|
-
"bookmarks",
|
|
122
|
-
"dms",
|
|
123
|
-
];
|
|
124
|
-
|
|
125
|
-
function expandHome(input: string) {
|
|
126
|
-
return input === "~" || input.startsWith("~/")
|
|
127
|
-
? path.join(os.homedir(), input.slice(2))
|
|
128
|
-
: input;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function resolvePath(input: string) {
|
|
132
|
-
return path.resolve(expandHome(input));
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function getDefaultAccountSyncAuditLogPath() {
|
|
136
|
-
return path.join(getBirdclawPaths().rootDir, "audit", "account-sync.jsonl");
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function getDefaultAccountSyncLockPath() {
|
|
140
|
-
return path.join(getBirdclawPaths().rootDir, "locks", "account-sync.lock");
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function messageFromError(error: unknown) {
|
|
144
|
-
return error instanceof Error ? error.message : String(error);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async function appendAuditEntry(logPath: string, entry: AccountSyncAuditEntry) {
|
|
148
|
-
await fs.mkdir(path.dirname(logPath), { recursive: true });
|
|
149
|
-
await fs.appendFile(logPath, `${JSON.stringify(entry)}\n`, "utf8");
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async function acquireLock(lockPath: string) {
|
|
153
|
-
await fs.mkdir(path.dirname(lockPath), { recursive: true });
|
|
154
|
-
try {
|
|
155
|
-
const handle = await fs.open(lockPath, "wx");
|
|
156
|
-
await handle.writeFile(
|
|
157
|
-
`${JSON.stringify({
|
|
158
|
-
pid: process.pid,
|
|
159
|
-
host: os.hostname(),
|
|
160
|
-
startedAt: new Date().toISOString(),
|
|
161
|
-
})}\n`,
|
|
162
|
-
"utf8",
|
|
163
|
-
);
|
|
164
|
-
await handle.close();
|
|
165
|
-
return async () => {
|
|
166
|
-
await fs.rm(lockPath, { force: true });
|
|
167
|
-
};
|
|
168
|
-
} catch (error) {
|
|
169
|
-
if (
|
|
170
|
-
typeof error === "object" &&
|
|
171
|
-
error !== null &&
|
|
172
|
-
"code" in error &&
|
|
173
|
-
error.code === "EEXIST"
|
|
174
|
-
) {
|
|
175
|
-
const stats = await fs.stat(lockPath).catch(() => undefined);
|
|
176
|
-
if (stats && Date.now() - stats.mtimeMs > DEFAULT_LOCK_STALE_MS) {
|
|
177
|
-
await fs.rm(lockPath, { force: true });
|
|
178
|
-
return acquireLock(lockPath);
|
|
179
|
-
}
|
|
180
|
-
return undefined;
|
|
181
|
-
}
|
|
182
|
-
throw error;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function readNumber(value: unknown, key: string): number {
|
|
187
|
-
if (!value || typeof value !== "object") return 0;
|
|
188
|
-
const raw = (value as Record<string, unknown>)[key];
|
|
189
|
-
return typeof raw === "number" && Number.isFinite(raw) ? raw : 0;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function readString(value: unknown, key: string) {
|
|
193
|
-
if (!value || typeof value !== "object") return undefined;
|
|
194
|
-
const raw = (value as Record<string, unknown>)[key];
|
|
195
|
-
return typeof raw === "string" ? raw : undefined;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function defaultAccountId(db: Database) {
|
|
199
|
-
const row = db
|
|
200
|
-
.prepare(
|
|
201
|
-
`
|
|
202
|
-
select id
|
|
203
|
-
from accounts
|
|
204
|
-
order by is_default desc, created_at asc
|
|
205
|
-
limit 1
|
|
206
|
-
`,
|
|
207
|
-
)
|
|
208
|
-
.get() as { id: string } | undefined;
|
|
209
|
-
return row?.id;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function isExplicitNonDefaultAccount(
|
|
213
|
-
db: Database,
|
|
214
|
-
account: string | undefined,
|
|
215
|
-
) {
|
|
216
|
-
if (!account) return false;
|
|
217
|
-
return account !== defaultAccountId(db);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function birdAccountError(kind: AccountSyncStepKind) {
|
|
221
|
-
return `Bird-backed ${kind} sync requires --allow-bird-account for non-default accounts; source matching cookies with --env-path first.`;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function resolveCollectionModeForAccount({
|
|
225
|
-
mode,
|
|
226
|
-
allowBirdAccount,
|
|
227
|
-
}: {
|
|
228
|
-
mode: TimelineCollectionMode;
|
|
229
|
-
allowBirdAccount: boolean | undefined;
|
|
230
|
-
}) {
|
|
231
|
-
if (allowBirdAccount || mode === "xurl") return mode;
|
|
232
|
-
return mode === "bird" ? undefined : "xurl";
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
async function runStep({
|
|
236
|
-
kind,
|
|
237
|
-
account,
|
|
238
|
-
mode,
|
|
239
|
-
limit,
|
|
240
|
-
maxPages,
|
|
241
|
-
refresh,
|
|
242
|
-
cacheTtlMs,
|
|
243
|
-
allowBirdAccount,
|
|
244
|
-
}: Required<
|
|
245
|
-
Pick<AccountSyncJobOptions, "mode" | "limit" | "maxPages" | "refresh">
|
|
246
|
-
> &
|
|
247
|
-
Pick<AccountSyncJobOptions, "account" | "cacheTtlMs" | "allowBirdAccount"> & {
|
|
248
|
-
kind: AccountSyncStepKind;
|
|
249
|
-
}): Promise<AccountSyncAuditStep> {
|
|
250
|
-
try {
|
|
251
|
-
if (kind === "timeline") {
|
|
252
|
-
const timelineMode =
|
|
253
|
-
mode === "auto"
|
|
254
|
-
? account
|
|
255
|
-
? allowBirdAccount
|
|
256
|
-
? "bird"
|
|
257
|
-
: "xurl"
|
|
258
|
-
: "auto"
|
|
259
|
-
: mode;
|
|
260
|
-
if (timelineMode === "bird" && !allowBirdAccount) {
|
|
261
|
-
return { kind, ok: false, count: 0, error: birdAccountError(kind) };
|
|
262
|
-
}
|
|
263
|
-
const result = await syncHomeTimeline({
|
|
264
|
-
account,
|
|
265
|
-
mode: timelineMode,
|
|
266
|
-
limit,
|
|
267
|
-
maxPages,
|
|
268
|
-
following: true,
|
|
269
|
-
refresh,
|
|
270
|
-
cacheTtlMs,
|
|
271
|
-
});
|
|
272
|
-
return {
|
|
273
|
-
kind,
|
|
274
|
-
ok: true,
|
|
275
|
-
count: readNumber(result, "count"),
|
|
276
|
-
source: readString(result, "source"),
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
if (kind === "mentions") {
|
|
280
|
-
if (mode === "bird" && !allowBirdAccount) {
|
|
281
|
-
return { kind, ok: false, count: 0, error: birdAccountError(kind) };
|
|
282
|
-
}
|
|
283
|
-
const mentionMode =
|
|
284
|
-
mode === "auto" ? (allowBirdAccount ? "auto" : "xurl") : mode;
|
|
285
|
-
const result = await syncMentions({
|
|
286
|
-
account,
|
|
287
|
-
mode: mentionMode,
|
|
288
|
-
limit,
|
|
289
|
-
maxPages,
|
|
290
|
-
refresh,
|
|
291
|
-
cacheTtlMs,
|
|
292
|
-
});
|
|
293
|
-
return {
|
|
294
|
-
kind,
|
|
295
|
-
ok: true,
|
|
296
|
-
count: readNumber(result, "count"),
|
|
297
|
-
source: readString(result, "source"),
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
if (kind === "mention-threads") {
|
|
301
|
-
const result = await syncMentionThreads({
|
|
302
|
-
account,
|
|
303
|
-
mode: "xurl",
|
|
304
|
-
limit: Math.min(30, limit),
|
|
305
|
-
delayMs: 1500,
|
|
306
|
-
timeoutMs: 15000,
|
|
307
|
-
});
|
|
308
|
-
return {
|
|
309
|
-
kind,
|
|
310
|
-
ok: true,
|
|
311
|
-
count: readNumber(result, "mergedTweets"),
|
|
312
|
-
source: readString(result, "source"),
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
if (kind === "dms") {
|
|
316
|
-
const dmMode = allowBirdAccount
|
|
317
|
-
? mode
|
|
318
|
-
: mode === "bird"
|
|
319
|
-
? undefined
|
|
320
|
-
: "xurl";
|
|
321
|
-
if (!dmMode) {
|
|
322
|
-
return { kind, ok: false, count: 0, error: birdAccountError(kind) };
|
|
323
|
-
}
|
|
324
|
-
const result = await syncDirectMessagesViaCachedBird({
|
|
325
|
-
account,
|
|
326
|
-
mode: dmMode,
|
|
327
|
-
limit: Math.min(50, limit),
|
|
328
|
-
refresh,
|
|
329
|
-
cacheTtlMs,
|
|
330
|
-
});
|
|
331
|
-
return {
|
|
332
|
-
kind,
|
|
333
|
-
ok: true,
|
|
334
|
-
count: readNumber(result, "messages"),
|
|
335
|
-
source: readString(result, "source"),
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const collectionKind = kind as TimelineCollectionKind;
|
|
340
|
-
const collectionMode = resolveCollectionModeForAccount({
|
|
341
|
-
mode,
|
|
342
|
-
allowBirdAccount,
|
|
343
|
-
});
|
|
344
|
-
if (!collectionMode) {
|
|
345
|
-
return { kind, ok: false, count: 0, error: birdAccountError(kind) };
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const result = await syncTimelineCollection({
|
|
349
|
-
kind: collectionKind,
|
|
350
|
-
account,
|
|
351
|
-
mode: collectionMode,
|
|
352
|
-
limit,
|
|
353
|
-
all: true,
|
|
354
|
-
maxPages,
|
|
355
|
-
refresh,
|
|
356
|
-
cacheTtlMs,
|
|
357
|
-
earlyStop: true,
|
|
358
|
-
});
|
|
359
|
-
return {
|
|
360
|
-
kind,
|
|
361
|
-
ok: true,
|
|
362
|
-
count: readNumber(result, "count"),
|
|
363
|
-
source: readString(result, "source"),
|
|
364
|
-
};
|
|
365
|
-
} catch (error) {
|
|
366
|
-
return {
|
|
367
|
-
kind,
|
|
368
|
-
ok: false,
|
|
369
|
-
count: 0,
|
|
370
|
-
error: messageFromError(error),
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
export async function runAccountSyncJob({
|
|
376
|
-
account,
|
|
377
|
-
steps = DEFAULT_STEPS,
|
|
378
|
-
mode = "auto",
|
|
379
|
-
limit = DEFAULT_ACCOUNT_SYNC_LIMIT,
|
|
380
|
-
maxPages = DEFAULT_ACCOUNT_SYNC_MAX_PAGES,
|
|
381
|
-
refresh = true,
|
|
382
|
-
cacheTtlMs,
|
|
383
|
-
allowBirdAccount,
|
|
384
|
-
logPath,
|
|
385
|
-
lockPath,
|
|
386
|
-
db,
|
|
387
|
-
}: AccountSyncJobOptions = {}): Promise<AccountSyncAuditEntry> {
|
|
388
|
-
ensureBirdclawDirs();
|
|
389
|
-
const database = db ?? getNativeDb({ seedDemoData: false });
|
|
390
|
-
const resolvedLogPath = resolvePath(
|
|
391
|
-
logPath ?? getDefaultAccountSyncAuditLogPath(),
|
|
392
|
-
);
|
|
393
|
-
const resolvedLockPath = resolvePath(
|
|
394
|
-
lockPath ?? getDefaultAccountSyncLockPath(),
|
|
395
|
-
);
|
|
396
|
-
const started = Date.now();
|
|
397
|
-
const startedAt = new Date(started).toISOString();
|
|
398
|
-
const options = {
|
|
399
|
-
...(account ? { account } : {}),
|
|
400
|
-
steps,
|
|
401
|
-
mode,
|
|
402
|
-
limit,
|
|
403
|
-
maxPages,
|
|
404
|
-
refresh,
|
|
405
|
-
...(cacheTtlMs === undefined ? {} : { cacheTtlMs }),
|
|
406
|
-
...(allowBirdAccount ? { allowBirdAccount } : {}),
|
|
407
|
-
};
|
|
408
|
-
const birdAccountAllowed =
|
|
409
|
-
!isExplicitNonDefaultAccount(database, account) ||
|
|
410
|
-
Boolean(allowBirdAccount);
|
|
411
|
-
|
|
412
|
-
const releaseLock = await acquireLock(resolvedLockPath);
|
|
413
|
-
if (!releaseLock) {
|
|
414
|
-
const finished = Date.now();
|
|
415
|
-
const entry: AccountSyncAuditEntry = {
|
|
416
|
-
job: "account-sync",
|
|
417
|
-
ok: true,
|
|
418
|
-
startedAt,
|
|
419
|
-
finishedAt: new Date(finished).toISOString(),
|
|
420
|
-
durationMs: finished - started,
|
|
421
|
-
host: os.hostname(),
|
|
422
|
-
pid: process.pid,
|
|
423
|
-
options,
|
|
424
|
-
steps: [],
|
|
425
|
-
skipped: "already-running",
|
|
426
|
-
};
|
|
427
|
-
await appendAuditEntry(resolvedLogPath, entry);
|
|
428
|
-
return entry;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const stepResults: AccountSyncAuditStep[] = [];
|
|
432
|
-
try {
|
|
433
|
-
for (const kind of steps) {
|
|
434
|
-
stepResults.push(
|
|
435
|
-
await runStep({
|
|
436
|
-
kind,
|
|
437
|
-
account,
|
|
438
|
-
mode,
|
|
439
|
-
limit,
|
|
440
|
-
maxPages,
|
|
441
|
-
refresh,
|
|
442
|
-
cacheTtlMs,
|
|
443
|
-
allowBirdAccount: birdAccountAllowed,
|
|
444
|
-
}),
|
|
445
|
-
);
|
|
446
|
-
}
|
|
447
|
-
const backup = await maybeAutoSyncBackup(database);
|
|
448
|
-
const finished = Date.now();
|
|
449
|
-
const entry: AccountSyncAuditEntry = {
|
|
450
|
-
job: "account-sync",
|
|
451
|
-
ok: stepResults.every((step) => step.ok),
|
|
452
|
-
startedAt,
|
|
453
|
-
finishedAt: new Date(finished).toISOString(),
|
|
454
|
-
durationMs: finished - started,
|
|
455
|
-
host: os.hostname(),
|
|
456
|
-
pid: process.pid,
|
|
457
|
-
options,
|
|
458
|
-
steps: stepResults,
|
|
459
|
-
backup,
|
|
460
|
-
};
|
|
461
|
-
await appendAuditEntry(resolvedLogPath, entry);
|
|
462
|
-
return entry;
|
|
463
|
-
} catch (error) {
|
|
464
|
-
const finished = Date.now();
|
|
465
|
-
const entry: AccountSyncAuditEntry = {
|
|
466
|
-
job: "account-sync",
|
|
467
|
-
ok: false,
|
|
468
|
-
startedAt,
|
|
469
|
-
finishedAt: new Date(finished).toISOString(),
|
|
470
|
-
durationMs: finished - started,
|
|
471
|
-
host: os.hostname(),
|
|
472
|
-
pid: process.pid,
|
|
473
|
-
options,
|
|
474
|
-
steps: stepResults,
|
|
475
|
-
error: messageFromError(error),
|
|
476
|
-
};
|
|
477
|
-
await appendAuditEntry(resolvedLogPath, entry);
|
|
478
|
-
return entry;
|
|
479
|
-
} finally {
|
|
480
|
-
await releaseLock();
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
function xmlEscape(value: string) {
|
|
485
|
-
return value
|
|
486
|
-
.replaceAll("&", "&")
|
|
487
|
-
.replaceAll("<", "<")
|
|
488
|
-
.replaceAll(">", ">");
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
function stringEntry(value: string) {
|
|
492
|
-
return `<string>${xmlEscape(value)}</string>`;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
function shellQuote(value: string) {
|
|
496
|
-
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
function buildProgramArguments({
|
|
500
|
-
program = "birdclaw",
|
|
501
|
-
account,
|
|
502
|
-
steps,
|
|
503
|
-
mode = "auto",
|
|
504
|
-
limit = DEFAULT_ACCOUNT_SYNC_LIMIT,
|
|
505
|
-
maxPages = DEFAULT_ACCOUNT_SYNC_MAX_PAGES,
|
|
506
|
-
refresh = true,
|
|
507
|
-
allowBirdAccount,
|
|
508
|
-
cacheTtlSeconds,
|
|
509
|
-
logPath,
|
|
510
|
-
envFile,
|
|
511
|
-
}: AccountSyncLaunchAgentOptions) {
|
|
512
|
-
const args =
|
|
513
|
-
path.isAbsolute(program) || program.includes("/")
|
|
514
|
-
? [program]
|
|
515
|
-
: ["/usr/bin/env", program];
|
|
516
|
-
args.push(
|
|
517
|
-
"--json",
|
|
518
|
-
"jobs",
|
|
519
|
-
"sync-account",
|
|
520
|
-
"--mode",
|
|
521
|
-
mode,
|
|
522
|
-
"--limit",
|
|
523
|
-
String(limit),
|
|
524
|
-
"--max-pages",
|
|
525
|
-
String(maxPages),
|
|
526
|
-
"--log",
|
|
527
|
-
resolvePath(logPath ?? getDefaultAccountSyncAuditLogPath()),
|
|
528
|
-
);
|
|
529
|
-
if (account) {
|
|
530
|
-
args.push("--account", account);
|
|
531
|
-
}
|
|
532
|
-
if (steps?.length) {
|
|
533
|
-
args.push("--steps", steps.join(","));
|
|
534
|
-
}
|
|
535
|
-
if (refresh) {
|
|
536
|
-
args.push("--refresh");
|
|
537
|
-
}
|
|
538
|
-
if (allowBirdAccount) {
|
|
539
|
-
args.push("--allow-bird-account");
|
|
540
|
-
}
|
|
541
|
-
if (cacheTtlSeconds !== undefined) {
|
|
542
|
-
args.push("--cache-ttl", String(cacheTtlSeconds));
|
|
543
|
-
}
|
|
544
|
-
if (envFile) {
|
|
545
|
-
const resolvedEnvFile = resolvePath(envFile);
|
|
546
|
-
return [
|
|
547
|
-
"/bin/bash",
|
|
548
|
-
"-lc",
|
|
549
|
-
[
|
|
550
|
-
"set -a",
|
|
551
|
-
`[ ! -f ${shellQuote(resolvedEnvFile)} ] || . ${shellQuote(resolvedEnvFile)}`,
|
|
552
|
-
"set +a",
|
|
553
|
-
`exec ${args.map(shellQuote).join(" ")}`,
|
|
554
|
-
].join("; "),
|
|
555
|
-
];
|
|
556
|
-
}
|
|
557
|
-
return args;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
export function buildAccountSyncLaunchAgentPlist(
|
|
561
|
-
options: AccountSyncLaunchAgentOptions = {},
|
|
562
|
-
) {
|
|
563
|
-
const label = options.label ?? DEFAULT_ACCOUNT_SYNC_LABEL;
|
|
564
|
-
const intervalSeconds =
|
|
565
|
-
options.intervalSeconds ?? DEFAULT_ACCOUNT_SYNC_INTERVAL_SECONDS;
|
|
566
|
-
const logPath = resolvePath(
|
|
567
|
-
options.logPath ?? getDefaultAccountSyncAuditLogPath(),
|
|
568
|
-
);
|
|
569
|
-
const stdoutPath = resolvePath(
|
|
570
|
-
options.stdoutPath ??
|
|
571
|
-
path.join(getBirdclawPaths().rootDir, "logs", "account-sync.out.log"),
|
|
572
|
-
);
|
|
573
|
-
const stderrPath = resolvePath(
|
|
574
|
-
options.stderrPath ??
|
|
575
|
-
path.join(getBirdclawPaths().rootDir, "logs", "account-sync.err.log"),
|
|
576
|
-
);
|
|
577
|
-
const programArguments = buildProgramArguments({ ...options, logPath });
|
|
578
|
-
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
579
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
580
|
-
<plist version="1.0">
|
|
581
|
-
<dict>
|
|
582
|
-
<key>Label</key>
|
|
583
|
-
${stringEntry(label)}
|
|
584
|
-
<key>ProgramArguments</key>
|
|
585
|
-
<array>
|
|
586
|
-
${programArguments.map(stringEntry).join("\n ")}
|
|
587
|
-
</array>
|
|
588
|
-
<key>StartInterval</key>
|
|
589
|
-
<integer>${String(intervalSeconds)}</integer>
|
|
590
|
-
<key>RunAtLoad</key>
|
|
591
|
-
<true/>
|
|
592
|
-
<key>StandardOutPath</key>
|
|
593
|
-
${stringEntry(stdoutPath)}
|
|
594
|
-
<key>StandardErrorPath</key>
|
|
595
|
-
${stringEntry(stderrPath)}
|
|
596
|
-
<key>EnvironmentVariables</key>
|
|
597
|
-
<dict>
|
|
598
|
-
<key>PATH</key>
|
|
599
|
-
${stringEntry(DEFAULT_LAUNCHD_PATH)}
|
|
600
|
-
</dict>
|
|
601
|
-
</dict>
|
|
602
|
-
</plist>
|
|
603
|
-
`;
|
|
604
|
-
return {
|
|
605
|
-
label,
|
|
606
|
-
intervalSeconds,
|
|
607
|
-
logPath,
|
|
608
|
-
...(options.envFile ? { envFile: resolvePath(options.envFile) } : {}),
|
|
609
|
-
stdoutPath,
|
|
610
|
-
stderrPath,
|
|
611
|
-
programArguments,
|
|
612
|
-
plist,
|
|
613
|
-
};
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
export async function installAccountSyncLaunchAgent(
|
|
617
|
-
options: AccountSyncLaunchAgentOptions = {},
|
|
618
|
-
): Promise<AccountSyncLaunchAgentInstallResult> {
|
|
619
|
-
ensureBirdclawDirs();
|
|
620
|
-
const agent = buildAccountSyncLaunchAgentPlist(options);
|
|
621
|
-
const launchAgentsDir = resolvePath(
|
|
622
|
-
options.launchAgentsDir ?? "~/Library/LaunchAgents",
|
|
623
|
-
);
|
|
624
|
-
const plistPath = path.join(launchAgentsDir, `${agent.label}.plist`);
|
|
625
|
-
await fs.mkdir(launchAgentsDir, { recursive: true });
|
|
626
|
-
await fs.mkdir(path.dirname(agent.logPath), { recursive: true });
|
|
627
|
-
await fs.mkdir(path.dirname(agent.stdoutPath), { recursive: true });
|
|
628
|
-
await fs.mkdir(path.dirname(agent.stderrPath), { recursive: true });
|
|
629
|
-
await fs.writeFile(plistPath, agent.plist, "utf8");
|
|
630
|
-
|
|
631
|
-
let loaded = false;
|
|
632
|
-
if (options.load !== false) {
|
|
633
|
-
await execFileAsync("launchctl", ["unload", plistPath]).catch(() => {});
|
|
634
|
-
await execFileAsync("launchctl", ["load", "-w", plistPath]);
|
|
635
|
-
loaded = true;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
return {
|
|
639
|
-
ok: true,
|
|
640
|
-
label: agent.label,
|
|
641
|
-
plistPath,
|
|
642
|
-
loaded,
|
|
643
|
-
programArguments: agent.programArguments,
|
|
644
|
-
logPath: agent.logPath,
|
|
645
|
-
stdoutPath: agent.stdoutPath,
|
|
646
|
-
stderrPath: agent.stderrPath,
|
|
647
|
-
intervalSeconds: agent.intervalSeconds,
|
|
648
|
-
...(agent.envFile ? { envFile: agent.envFile } : {}),
|
|
649
|
-
};
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
export function parseAccountSyncSteps(value: string | undefined) {
|
|
653
|
-
if (!value) return undefined;
|
|
654
|
-
const valid = new Set<AccountSyncStepKind>(DEFAULT_STEPS);
|
|
655
|
-
const steps = value
|
|
656
|
-
.split(",")
|
|
657
|
-
.map((step) => step.trim())
|
|
658
|
-
.filter(Boolean);
|
|
659
|
-
if (steps.length === 0) return undefined;
|
|
660
|
-
for (const step of steps) {
|
|
661
|
-
if (!valid.has(step as AccountSyncStepKind)) {
|
|
662
|
-
throw new Error(`--steps must contain ${Array.from(valid).join(", ")}`);
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
return steps as AccountSyncStepKind[];
|
|
666
|
-
}
|