birdclaw 0.4.1 → 0.5.1
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 +52 -0
- package/README.md +113 -7
- package/bin/birdclaw.mjs +50 -11
- package/package.json +30 -28
- package/playwright.config.ts +1 -0
- package/public/birdclaw-mark.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/logo192.png +0 -0
- package/public/logo512.png +0 -0
- package/public/manifest.json +2 -2
- package/scripts/browser-perf.mjs +399 -0
- package/scripts/build-docs-site.mjs +940 -0
- package/scripts/docs-site-assets.mjs +311 -0
- package/scripts/run-vitest.mjs +21 -0
- package/scripts/sanitize-node-options.mjs +23 -0
- package/scripts/start-test-server.mjs +29 -0
- package/src/cli.ts +496 -19
- package/src/components/AppNav.tsx +66 -29
- package/src/components/AvatarChip.tsx +10 -5
- package/src/components/BrandMark.tsx +67 -0
- package/src/components/ConversationThread.tsx +126 -0
- package/src/components/DmWorkspace.tsx +118 -105
- package/src/components/EmbeddedTweetCard.tsx +20 -14
- package/src/components/FeedState.tsx +147 -0
- package/src/components/InboxCard.tsx +104 -90
- package/src/components/LinkPreviewCard.tsx +270 -0
- package/src/components/ProfilePreview.tsx +8 -3
- package/src/components/SavedTimelineView.tsx +89 -71
- package/src/components/SyncNowButton.tsx +105 -0
- package/src/components/ThemeSlider.tsx +10 -59
- package/src/components/TimelineCard.tsx +326 -86
- package/src/components/TimelineRouteFrame.tsx +156 -0
- package/src/components/TweetMediaGrid.tsx +120 -23
- package/src/components/TweetRichText.tsx +19 -4
- package/src/components/useTimelineRouteData.ts +137 -0
- package/src/lib/api-client.ts +229 -0
- package/src/lib/archive-finder.ts +24 -20
- package/src/lib/archive-import.ts +1582 -67
- package/src/lib/authored-live.ts +1074 -0
- package/src/lib/backup.ts +316 -14
- package/src/lib/bird-actions.ts +1 -10
- package/src/lib/bird-command.ts +57 -0
- package/src/lib/bird.ts +89 -5
- package/src/lib/config.ts +1 -1
- package/src/lib/conversation-surface.ts +174 -0
- package/src/lib/db.ts +193 -4
- package/src/lib/follow-graph.ts +1053 -0
- package/src/lib/link-index.ts +11 -98
- package/src/lib/link-insights.ts +834 -0
- package/src/lib/link-preview-metadata.ts +334 -0
- package/src/lib/media-fetch.ts +787 -0
- package/src/lib/media-includes.ts +165 -0
- package/src/lib/mention-threads-live.ts +535 -43
- package/src/lib/mentions-live.ts +623 -19
- package/src/lib/moderation-target.ts +6 -0
- package/src/lib/profile-hydration.ts +1 -1
- package/src/lib/profile-resolver.ts +115 -1
- package/src/lib/queries.ts +326 -35
- package/src/lib/seed.ts +127 -8
- package/src/lib/timeline-collections-live.ts +145 -22
- package/src/lib/timeline-live.ts +10 -15
- package/src/lib/tweet-account-edges.ts +9 -2
- package/src/lib/tweet-render.ts +97 -0
- package/src/lib/types.ts +185 -2
- package/src/lib/ui.ts +383 -147
- package/src/lib/url-expansion-store.ts +110 -0
- package/src/lib/url-expansion.ts +20 -3
- package/src/lib/web-sync.ts +443 -0
- package/src/lib/x-profile.ts +7 -2
- package/src/lib/xurl.ts +296 -12
- package/src/routeTree.gen.ts +126 -0
- package/src/routes/__root.tsx +7 -3
- package/src/routes/api/conversation.tsx +34 -0
- package/src/routes/api/link-insights.tsx +79 -0
- package/src/routes/api/link-preview.tsx +43 -0
- package/src/routes/api/profile-hydrate.tsx +51 -0
- package/src/routes/api/sync.tsx +59 -0
- package/src/routes/blocks.tsx +111 -86
- package/src/routes/dms.tsx +172 -87
- package/src/routes/inbox.tsx +98 -86
- package/src/routes/index.tsx +22 -115
- package/src/routes/links.tsx +928 -0
- package/src/routes/mentions.tsx +22 -115
- package/src/styles.css +169 -43
- package/vite.config.ts +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,51 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.5.1 - 2026-05-15
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Harden the published CLI wrapper and release checks so the packaged `birdclaw` binary avoids `tsx` CLI IPC startup and stays covered by lint, format, and smoke tests.
|
|
10
|
+
- Forward shutdown signals through the published CLI and bundled web server, and include referenced script helpers in npm packages.
|
|
11
|
+
- Keep the selected DM conversation visible while its thread refreshes so the reply composer no longer flashes away mid-action.
|
|
12
|
+
- Send the selected web account through manual sync controls so multi-account timelines sync the intended profile.
|
|
13
|
+
- Run web sync requests as background jobs with status polling so the UI no longer holds one blocking sync request open.
|
|
14
|
+
- Add typed web API fetch handling and explicit DMs loading/error/empty states so failed local reads surface cleanly.
|
|
15
|
+
- Add explicit web app sync controls for home timeline, mentions, likes, bookmarks, and DMs so fresh live data can be pulled without leaving the UI.
|
|
16
|
+
- Refine the web app sidebar tagline and theme selector so the brand chrome reads more clearly in compact layouts.
|
|
17
|
+
- Add shared web feed loading/error/empty states with timeline-shaped skeleton rows and move conversation expansion into a cached single-thread surface with hover prefetch.
|
|
18
|
+
- Use the Birdclaw crab-bird mark in the web app chrome, loading states, and empty states; soften dark-mode contrast and replace text-only reply warnings with conversation/replied indicators.
|
|
19
|
+
- Allow the local web app to respond when Tailscale Serve forwards requests through the `clawmac.sheep-coho.ts.net` hostname.
|
|
20
|
+
- Speed up the default home timeline load on large local databases and keep malformed archived media URL entities from crashing the web timeline.
|
|
21
|
+
- Preserve tweet media aspect ratios, open timeline media in an inline viewer, and suppress duplicate media URL cards.
|
|
22
|
+
|
|
23
|
+
## 0.5.0 - 2026-05-15
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- Add `birdclaw import archive --select` for importing targeted archive slices while preserving unselected local data.
|
|
28
|
+
- Add `birdclaw sync authored` for filling own-tweet gaps from `xurl` after the archive cutoff. Thanks @cavit99.
|
|
29
|
+
- Add live `sync mentions` and `sync mention-threads --mode xurl` ingestion for current mention data and conversation context. Thanks @cavit99.
|
|
30
|
+
- Add `birdclaw media fetch` plus archive bundled-media extraction and live media variant persistence for local originals caching. Thanks @cavit99.
|
|
31
|
+
- Add a `/links` web lane for Hacker News-style top URL and video-provider insights with today, week, month, year, and all-time ranges.
|
|
32
|
+
- Import archive `follower.js`/`following.js` files into the local follow graph and add archive-authored tweet edges so fresh archive imports are immediately queryable without live sync. Thanks @cavit99.
|
|
33
|
+
- Add cache-first followers/following sync, local follow graph queries, and backup/export support for graph snapshots and churn events. Thanks @ma08.
|
|
34
|
+
- Hydrate missing link-discussion profile avatars through `bird`/`xurl` so hover sheets can upgrade archive placeholders into real profile cards.
|
|
35
|
+
- Add inline tweet conversation expansion in the web timeline, preserving the selected reply's parent chain before broad thread context.
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- Update npm dependencies, including React, Vite, Vitest, Playwright, Tailwind, Kysely, TanStack packages, oxlint, and oxfmt.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- Seed demo link insight data before direct `/links` route loads, so the lane is populated even when it is the first web route opened.
|
|
44
|
+
- Isolate the default `bird` command config test from the maintainer's local `~/.birdclaw/config.json`.
|
|
45
|
+
- Skip non-numeric archive placeholder IDs such as self-DM conversation IDs when hydrating profiles through X, so one malformed local ID no longer aborts the batch. Thanks @nfarina.
|
|
46
|
+
- Include expanded short URLs and link occurrences in Git-friendly backups so linked-tweet search survives backup restore.
|
|
47
|
+
- Prefer `bird` for follow graph sync in `auto` mode, keeping `xurl` as an explicit fallback for accounts where OAuth2 follow reads work.
|
|
48
|
+
- Update the docs site and app icons to use the Birdclaw crab-bird mark instead of the generic bird logo.
|
|
49
|
+
|
|
5
50
|
## 0.4.1 - 2026-05-11
|
|
6
51
|
|
|
7
52
|
### Added
|
|
@@ -51,6 +96,13 @@
|
|
|
51
96
|
- Allow Playwright e2e runs to use an alternate local port when `3000` is already occupied.
|
|
52
97
|
- Replace maintainer-local documentation links with repo-relative links and align the setup docs with the Node version file. Thanks @stainlu.
|
|
53
98
|
|
|
99
|
+
## Unreleased
|
|
100
|
+
|
|
101
|
+
### Fixed
|
|
102
|
+
|
|
103
|
+
- Fix live `xurl` status detection when the CLI is installed but not authenticated; thanks @kyupark.
|
|
104
|
+
- Default local `bird` integration to `bird` on PATH and report stale configured command paths with setup guidance.
|
|
105
|
+
|
|
54
106
|
## 0.2.1 - 2026-04-27
|
|
55
107
|
|
|
56
108
|
### Changed
|
package/README.md
CHANGED
|
@@ -20,20 +20,27 @@ Status: WIP. Real and usable. Not done. Expect schema churn, transport gaps, and
|
|
|
20
20
|
- one shared SQLite DB for multiple accounts, with canonical tweets/profiles and account-scoped timeline/collection edges
|
|
21
21
|
- FTS5 search over tweets and DMs
|
|
22
22
|
- archive autodiscovery on macOS
|
|
23
|
-
- archive import for tweets, likes, profiles, and full DMs
|
|
23
|
+
- archive import for tweets, likes, followers/following, profiles, and full DMs
|
|
24
|
+
- selective archive re-imports for one stale slice without wiping the rest of the local store
|
|
24
25
|
- archive import for bookmark exports when present
|
|
25
|
-
-
|
|
26
|
+
- archive import streams bundled media files into the local originals cache and extracts `video_info.variants[]` for video and animated-GIF rows
|
|
27
|
+
- live authored sync through `xurl`, plus likes and bookmarks through `xurl` or `bird`
|
|
28
|
+
- cache-first followers/following sync through `bird` or `xurl`
|
|
29
|
+
- local follow graph queries for top followers, unfollows, mutuals, and non-mutual following
|
|
26
30
|
- Git-friendly text backups with yearly tweet shards and per-conversation DM shards
|
|
27
31
|
- profile hydration from live Twitter metadata
|
|
28
32
|
- profile-change history, affiliation badge edges, and extracted bio entities for local identity lookups
|
|
29
33
|
- local avatar cache
|
|
30
34
|
- local media cache root under `~/.birdclaw`
|
|
35
|
+
- live syncers persist tweet media variants so `media fetch` can pull originals from `pbs.twimg.com` and `video.twimg.com` on a separate schedule
|
|
36
|
+
- `media fetch` reuses bytes already extracted by `import archive` before falling back to CDN
|
|
31
37
|
|
|
32
38
|
### Web UI
|
|
33
39
|
|
|
34
40
|
- `Home` timeline
|
|
35
41
|
- `Mentions` queue
|
|
36
42
|
- `Likes` and `Bookmarks` review lanes
|
|
43
|
+
- `Links` for Hacker News-style top URLs, video-provider links, and the comments around them across today/week/month/year/all-time windows
|
|
37
44
|
- `DMs` workspace with two-column layout
|
|
38
45
|
- `Inbox` for mixed mention + DM triage
|
|
39
46
|
- `Blocks` for local blocklist maintenance
|
|
@@ -74,7 +81,7 @@ Status: WIP. Real and usable. Not done. Expect schema churn, transport gaps, and
|
|
|
74
81
|
## Still In Progress
|
|
75
82
|
|
|
76
83
|
- broader resumable live sync beyond the targeted paths already wired
|
|
77
|
-
-
|
|
84
|
+
- thumbnail generation on top of the originals cache
|
|
78
85
|
- richer multi-account UX
|
|
79
86
|
- more complete transport coverage
|
|
80
87
|
- more archive edge-case handling
|
|
@@ -102,6 +109,7 @@ Important paths:
|
|
|
102
109
|
|
|
103
110
|
- DB: `~/.birdclaw/birdclaw.sqlite`
|
|
104
111
|
- media cache: `~/.birdclaw/media`
|
|
112
|
+
- archive-extracted media: `~/.birdclaw/media/originals/archive/<kind>/<id>/<filename>` where `<kind>` is one of `tweets`, `dms`, `community`, `deleted`, `profile`, `moments`, `dmGroup`
|
|
105
113
|
- avatar cache: `~/.birdclaw/media/thumbs/avatars`
|
|
106
114
|
- Playwright test home: `.playwright-home`
|
|
107
115
|
|
|
@@ -111,6 +119,53 @@ Override the root:
|
|
|
111
119
|
export BIRDCLAW_HOME=/path/to/custom/root
|
|
112
120
|
```
|
|
113
121
|
|
|
122
|
+
### Media fetch
|
|
123
|
+
|
|
124
|
+
`birdclaw media fetch` fills the local originals cache at
|
|
125
|
+
`~/.birdclaw/media/originals/<media_key>.<ext>` for tweet media URLs already
|
|
126
|
+
stored in `tweets.media_json`. Images come from `pbs.twimg.com`, videos and
|
|
127
|
+
animated GIFs from `video.twimg.com` (highest-bitrate mp4 variant; HLS-only
|
|
128
|
+
media is skipped).
|
|
129
|
+
|
|
130
|
+
Live syncers (`sync mentions`, `sync mention-threads`, `sync likes`, `sync
|
|
131
|
+
bookmarks`, `sync timeline`) persist `media_json` with `variants[]` ride-along
|
|
132
|
+
metadata so `media fetch` has URLs to download from. Archive-imported tweets
|
|
133
|
+
already carry that shape. Before falling back to HTTP, `media fetch` looks for
|
|
134
|
+
bytes already extracted by `import archive` under
|
|
135
|
+
`~/.birdclaw/media/originals/archive/tweets/<tweetId>/` and copies those into
|
|
136
|
+
the canonical path; reuses are counted in the JSON output as
|
|
137
|
+
`reused_from_archive` and never spend CDN bandwidth.
|
|
138
|
+
|
|
139
|
+
Legal posture: this is a respectful client-rendering cache, not a scraper. The
|
|
140
|
+
command never enumerates, crawls, or derives Twitter/X CDN URLs. It only
|
|
141
|
+
fetches URLs that birdclaw already has from an archive or API/live sync
|
|
142
|
+
record, skips files that already exist locally, sends a birdclaw user agent,
|
|
143
|
+
paces image requests sequentially by default, caps optional image parallelism
|
|
144
|
+
at five, runs video downloads serially with their own `--video-pacing-ms`,
|
|
145
|
+
streams response bodies to a `.tmp` file with `Range: bytes=<size>-` resume,
|
|
146
|
+
caps each file at `--max-bytes` (100MB default), backs off on `429`, and
|
|
147
|
+
relies on the local file cache for idempotency.
|
|
148
|
+
|
|
149
|
+
Thumbnail generation and automatic invocation from sync commands are
|
|
150
|
+
intentionally left out. Run it separately, for example from cron or launchd
|
|
151
|
+
every few hours:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
birdclaw media fetch --json
|
|
155
|
+
birdclaw media fetch --dry-run --limit 20
|
|
156
|
+
birdclaw media fetch --include-video --video-pacing-ms 1500 --max-bytes 209715200 --json
|
|
157
|
+
birdclaw media fetch --no-include-video --parallel 3 --pacing-ms 250 --json
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Notes:
|
|
161
|
+
|
|
162
|
+
- `--include-video` is on by default; pass `--no-include-video` for images
|
|
163
|
+
only
|
|
164
|
+
- `--kind`, `--since`, and `--limit` scope which tweet rows are inspected
|
|
165
|
+
- `--parallel` applies to image fetches only; video fetches stay serial
|
|
166
|
+
- JSON output reports `images_fetched`, `videos_fetched`, `gifs_fetched`,
|
|
167
|
+
`reused_from_archive`, and per-kind byte counters
|
|
168
|
+
|
|
114
169
|
## Requirements
|
|
115
170
|
|
|
116
171
|
- Node `25.8.1` or Node 26.x
|
|
@@ -163,9 +218,26 @@ Find and import an archive:
|
|
|
163
218
|
birdclaw archive find --json
|
|
164
219
|
birdclaw import archive --json
|
|
165
220
|
birdclaw import archive ~/Downloads/twitter-archive-2025.zip --json
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Optional profile hydration can improve bios, follower counts, and avatars, but it performs live X profile reads and can spend API credits on large archives:
|
|
224
|
+
|
|
225
|
+
```bash
|
|
166
226
|
birdclaw import hydrate-profiles --json
|
|
167
227
|
```
|
|
168
228
|
|
|
229
|
+
`import archive` is idempotent. Re-running parses follower/following edges into the local follow graph, streams bundled media files under `data/tweets_media/`, `data/direct_messages_media/`, and the other archive media folders into `~/.birdclaw/media/originals/archive/<kind>/<id>/`, and pulls `video_info.variants[]` so archive video and animated-GIF rows carry mp4 URLs for the live media fetcher. Already-extracted files are skipped when size matches.
|
|
230
|
+
|
|
231
|
+
Re-import only one part of a newer archive when you already have live or local data you want to keep:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
birdclaw import archive ~/Downloads/twitter-archive-2026.zip --select tweets --json
|
|
235
|
+
birdclaw import archive ~/Downloads/twitter-archive-2026.zip --select likes,bookmarks --json
|
|
236
|
+
birdclaw import archive ~/Downloads/twitter-archive-2026.zip --select directMessages --json
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Valid `--select` slices are `tweets`, `likes`, `bookmarks`, `profiles`, `directMessages`, `followers`, and `following`. `dms` and `direct-messages` are accepted aliases for `directMessages`.
|
|
240
|
+
|
|
169
241
|
Back up the local SQLite store as canonical JSONL text:
|
|
170
242
|
|
|
171
243
|
```bash
|
|
@@ -184,6 +256,10 @@ Start the app:
|
|
|
184
256
|
birdclaw serve
|
|
185
257
|
```
|
|
186
258
|
|
|
259
|
+
Use the Sync button in Home, Mentions, Likes, Bookmarks, or DMs to run the matching live sync from the web UI and then reload the local view. These controls are explicit because live reads can be slow, auth-dependent, or rate-limited.
|
|
260
|
+
|
|
261
|
+
When running behind a trusted reverse proxy such as Tailscale Serve, add any extra proxy hostnames to `BIRDCLAW_ALLOWED_HOSTS`. The clawmac Tailscale hostname is allowed by default.
|
|
262
|
+
|
|
187
263
|
First moderation pass:
|
|
188
264
|
|
|
189
265
|
```bash
|
|
@@ -204,21 +280,47 @@ pnpm cli search tweets --liked --limit 20 --json
|
|
|
204
280
|
pnpm cli search tweets --bookmarked --limit 20 --json
|
|
205
281
|
```
|
|
206
282
|
|
|
207
|
-
### Sync likes, bookmarks,
|
|
283
|
+
### Sync authored tweets, likes, bookmarks, home timeline, and mentions
|
|
208
284
|
|
|
209
|
-
`auto` tries `xurl` first, then falls back to `bird`. Use `bird` directly when the API path is unavailable for the account/token you have locally.
|
|
285
|
+
`auto` tries `xurl` first for likes/bookmarks, then falls back to `bird`. Use `bird` directly when the API path is unavailable for the account/token you have locally. For repeated xurl collection syncs, add `--early-stop` to stop paging once a whole page already exists locally; without `--all` or `--max-pages`, it caps at 10 pages.
|
|
210
286
|
|
|
211
287
|
```bash
|
|
288
|
+
pnpm cli sync authored --mode xurl --limit 100 --json
|
|
212
289
|
pnpm cli sync likes --mode auto --limit 100 --refresh --json
|
|
213
290
|
pnpm cli sync bookmarks --mode auto --limit 100 --refresh --json
|
|
291
|
+
pnpm cli sync likes --mode auto --limit 100 --max-pages 5 --early-stop --refresh --json
|
|
292
|
+
pnpm cli sync bookmarks --mode auto --limit 100 --max-pages 5 --early-stop --refresh --json
|
|
214
293
|
pnpm cli sync bookmarks --mode bird --all --max-pages 5 --limit 100 --refresh --json
|
|
215
294
|
pnpm cli sync timeline --limit 100 --refresh --json
|
|
295
|
+
pnpm cli sync mentions --mode xurl --limit 100 --max-pages 3 --refresh --json
|
|
216
296
|
pnpm cli sync mention-threads --limit 30 --delay-ms 1500 --timeout-ms 15000 --json
|
|
217
297
|
```
|
|
218
298
|
|
|
299
|
+
Mention context is a two-step sync pipeline: run `sync mentions` to ingest recent mention rows with `kind='mention'`, then run `sync mention-threads --mode xurl` to fill parent/root conversation context.
|
|
300
|
+
|
|
301
|
+
### Follow graph queries
|
|
302
|
+
|
|
303
|
+
Follow graph sync is cache-first and defaults to dry-run so repeated agent queries do not keep spending live reads. `auto` prefers `bird` for this path and falls back to `xurl`.
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
pnpm cli sync followers --json
|
|
307
|
+
pnpm cli sync following --json
|
|
308
|
+
pnpm cli sync followers --yes --json
|
|
309
|
+
pnpm cli sync following --yes --json
|
|
310
|
+
pnpm cli sync followers --mode bird --yes --json
|
|
311
|
+
pnpm cli graph summary --json
|
|
312
|
+
pnpm cli graph events --since 2026-05-01 --json
|
|
313
|
+
pnpm cli graph top-followers --limit 20 --json
|
|
314
|
+
pnpm cli graph unfollowed --date 2026-05-01 --json
|
|
315
|
+
pnpm cli graph non-mutual-following --sort followers --limit 100 --json
|
|
316
|
+
pnpm cli graph mutuals --json
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Use `--refresh` only when you intentionally want a new live fetch. The `graph` commands are local SQLite reads and never call X. See [follow-graph.md](docs/follow-graph.md) for long-term agent usage notes.
|
|
320
|
+
|
|
219
321
|
### Export mentions for agents
|
|
220
322
|
|
|
221
|
-
Default `birdclaw` mode
|
|
323
|
+
Default `birdclaw` mode exports DB-backed mention items with `text`, `plainText`, `markdown`, author metadata, and canonical URLs:
|
|
222
324
|
|
|
223
325
|
```bash
|
|
224
326
|
pnpm cli mentions export "agent" --unreplied --limit 10
|
|
@@ -260,7 +362,8 @@ Notes:
|
|
|
260
362
|
- `actions.transport` accepts `auto`, `bird`, or `xurl`
|
|
261
363
|
- `bird` mode uses your local `bird` CLI and caches its mentions output into birdclaw's canonical store
|
|
262
364
|
- filters still work in `xurl` mode; filtered payloads are rebuilt from the local canonical store after sync
|
|
263
|
-
- `sync
|
|
365
|
+
- `sync authored`, `sync mentions`, `sync mention-threads`, `sync likes`, `sync bookmarks`, and `sync timeline` store live results in the canonical local store; per-account authored/home/mention/like/bookmark membership is kept as edges so shared tweets do not clobber account ownership
|
|
366
|
+
- the web UI has explicit Sync buttons for home timeline, mentions, likes, bookmarks, and DMs; they call the same sync paths and then reload the local DB-backed view
|
|
264
367
|
|
|
265
368
|
### Research bookmarks and threads
|
|
266
369
|
|
|
@@ -510,12 +613,14 @@ pnpm test
|
|
|
510
613
|
pnpm coverage
|
|
511
614
|
pnpm build
|
|
512
615
|
pnpm e2e
|
|
616
|
+
pnpm perf:browser -- --scenario=links,links-toggle --iterations=5
|
|
513
617
|
```
|
|
514
618
|
|
|
515
619
|
Current bar:
|
|
516
620
|
|
|
517
621
|
- branch coverage above `80%`
|
|
518
622
|
- Playwright coverage for core UI flows
|
|
623
|
+
- browser perf smoke reports ready/action timings plus API endpoint fan-out
|
|
519
624
|
|
|
520
625
|
## CI
|
|
521
626
|
|
|
@@ -533,3 +638,4 @@ Workflow: [ci.yml](.github/workflows/ci.yml)
|
|
|
533
638
|
- [spec.md](docs/spec.md)
|
|
534
639
|
- [cli.md](docs/cli.md)
|
|
535
640
|
- [data-architecture.md](docs/data-architecture.md)
|
|
641
|
+
- [follow-graph.md](docs/follow-graph.md)
|
package/bin/birdclaw.mjs
CHANGED
|
@@ -1,30 +1,69 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
6
|
|
|
7
7
|
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
8
8
|
const require = createRequire(import.meta.url);
|
|
9
|
-
const
|
|
9
|
+
const tsxLoader = pathToFileURL(require.resolve("tsx")).href;
|
|
10
10
|
const birdclawCli = join(packageRoot, "src", "cli.ts");
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const child = spawn(
|
|
13
13
|
process.execPath,
|
|
14
|
-
[
|
|
14
|
+
["--import", tsxLoader, birdclawCli, ...process.argv.slice(2)],
|
|
15
15
|
{
|
|
16
16
|
stdio: "inherit",
|
|
17
17
|
env: process.env,
|
|
18
|
+
detached: process.platform !== "win32",
|
|
18
19
|
},
|
|
19
20
|
);
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const forwardedSignals = ["SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT"];
|
|
23
|
+
|
|
24
|
+
function removeSignalHandlers() {
|
|
25
|
+
for (const signal of forwardedSignals) {
|
|
26
|
+
process.removeListener(signal, forwardSignal);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function forwardSignal(signal) {
|
|
31
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
32
|
+
signalChild(signal);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function signalChild(signal) {
|
|
37
|
+
if (child.pid === undefined) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const targetPid = process.platform === "win32" ? child.pid : -child.pid;
|
|
41
|
+
try {
|
|
42
|
+
process.kill(targetPid, signal);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error?.code !== "ESRCH") {
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
24
48
|
}
|
|
25
49
|
|
|
26
|
-
|
|
27
|
-
process.
|
|
50
|
+
for (const signal of forwardedSignals) {
|
|
51
|
+
process.on(signal, forwardSignal);
|
|
28
52
|
}
|
|
29
53
|
|
|
30
|
-
|
|
54
|
+
child.on("error", (error) => {
|
|
55
|
+
removeSignalHandlers();
|
|
56
|
+
console.error(error.message);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
child.on("exit", (code, signal) => {
|
|
61
|
+
removeSignalHandlers();
|
|
62
|
+
|
|
63
|
+
if (signal) {
|
|
64
|
+
process.kill(process.pid, signal);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
process.exit(code ?? 0);
|
|
69
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "birdclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Local Twitter memory in SQLite for archives, DMs, likes, bookmarks, and moderation",
|
|
5
5
|
"homepage": "https://github.com/steipete/birdclaw#readme",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"bin/",
|
|
16
|
+
"scripts/",
|
|
16
17
|
"src/",
|
|
17
18
|
"!src/**/*.test.ts",
|
|
18
19
|
"!src/**/*.test.tsx",
|
|
@@ -38,54 +39,55 @@
|
|
|
38
39
|
"dev": "vite dev --port 3000",
|
|
39
40
|
"build": "vite build",
|
|
40
41
|
"preview": "vite preview",
|
|
41
|
-
"test": "
|
|
42
|
-
"coverage": "
|
|
42
|
+
"test": "node ./scripts/run-vitest.mjs run",
|
|
43
|
+
"coverage": "node ./scripts/run-vitest.mjs run --coverage",
|
|
43
44
|
"e2e": "playwright test",
|
|
44
45
|
"typecheck": "tsgo --noEmit",
|
|
45
|
-
"format": "oxfmt --write src scripts playwright vite.config.ts vitest.config.ts playwright.config.ts",
|
|
46
|
-
"format:check": "oxfmt --check src scripts playwright vite.config.ts vitest.config.ts playwright.config.ts",
|
|
47
|
-
"lint": "oxlint --import-plugin --node-plugin --vitest-plugin --deny-warnings -A require-mock-type-parameters -A no-control-regex src scripts playwright vite.config.ts vitest.config.ts playwright.config.ts",
|
|
46
|
+
"format": "oxfmt --write bin src scripts playwright vite.config.ts vitest.config.ts playwright.config.ts",
|
|
47
|
+
"format:check": "oxfmt --check bin src scripts playwright vite.config.ts vitest.config.ts playwright.config.ts",
|
|
48
|
+
"lint": "oxlint --import-plugin --node-plugin --vitest-plugin --deny-warnings -A require-mock-type-parameters -A no-control-regex bin src scripts playwright vite.config.ts vitest.config.ts playwright.config.ts",
|
|
48
49
|
"check": "pnpm run format:check && pnpm run lint",
|
|
50
|
+
"perf:browser": "node ./scripts/browser-perf.mjs",
|
|
49
51
|
"cli": "tsx src/cli.ts",
|
|
50
52
|
"docs:site": "node scripts/build-docs-site.mjs"
|
|
51
53
|
},
|
|
52
54
|
"dependencies": {
|
|
53
|
-
"@steipete/sweet-cookie": "^0.
|
|
54
|
-
"@tailwindcss/vite": "^4.
|
|
55
|
-
"@tanstack/devtools-vite": "0.
|
|
56
|
-
"@tanstack/react-devtools": "0.10.
|
|
57
|
-
"@tanstack/react-router": "1.169.
|
|
55
|
+
"@steipete/sweet-cookie": "^0.3.0",
|
|
56
|
+
"@tailwindcss/vite": "^4.3.0",
|
|
57
|
+
"@tanstack/devtools-vite": "0.7.0",
|
|
58
|
+
"@tanstack/react-devtools": "0.10.5",
|
|
59
|
+
"@tanstack/react-router": "1.169.2",
|
|
58
60
|
"@tanstack/react-router-devtools": "1.166.13",
|
|
59
61
|
"@tanstack/react-router-ssr-query": "1.166.12",
|
|
60
|
-
"@tanstack/react-start": "1.167.
|
|
61
|
-
"@tanstack/router-plugin": "^1.167.
|
|
62
|
+
"@tanstack/react-start": "1.167.65",
|
|
63
|
+
"@tanstack/router-plugin": "^1.167.35",
|
|
62
64
|
"@vitejs/plugin-react": "^6.0.1",
|
|
63
65
|
"commander": "^14.0.3",
|
|
64
|
-
"kysely": "^0.
|
|
65
|
-
"lucide-react": "^1.
|
|
66
|
-
"react": "^19.2.
|
|
67
|
-
"react-dom": "^19.2.
|
|
68
|
-
"tailwindcss": "^4.
|
|
69
|
-
"tsx": "^4.
|
|
70
|
-
"vite": "^8.0.
|
|
71
|
-
"zod": "^4.4.
|
|
66
|
+
"kysely": "^0.29.0",
|
|
67
|
+
"lucide-react": "^1.16.0",
|
|
68
|
+
"react": "^19.2.6",
|
|
69
|
+
"react-dom": "^19.2.6",
|
|
70
|
+
"tailwindcss": "^4.3.0",
|
|
71
|
+
"tsx": "^4.22.0",
|
|
72
|
+
"vite": "^8.0.13",
|
|
73
|
+
"zod": "^4.4.3"
|
|
72
74
|
},
|
|
73
75
|
"devDependencies": {
|
|
74
|
-
"@playwright/test": "^1.
|
|
76
|
+
"@playwright/test": "^1.60.0",
|
|
75
77
|
"@tailwindcss/typography": "^0.5.19",
|
|
76
78
|
"@testing-library/dom": "^10.4.1",
|
|
77
79
|
"@testing-library/jest-dom": "^6.9.1",
|
|
78
80
|
"@testing-library/react": "^16.3.2",
|
|
79
|
-
"@types/node": "^25.
|
|
81
|
+
"@types/node": "^25.8.0",
|
|
80
82
|
"@types/react": "^19.2.14",
|
|
81
83
|
"@types/react-dom": "^19.2.3",
|
|
82
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
83
|
-
"@vitest/coverage-v8": "^4.1.
|
|
84
|
+
"@typescript/native-preview": "7.0.0-dev.20260514.1",
|
|
85
|
+
"@vitest/coverage-v8": "^4.1.6",
|
|
84
86
|
"jsdom": "^29.1.1",
|
|
85
|
-
"oxfmt": "^0.
|
|
86
|
-
"oxlint": "^1.
|
|
87
|
+
"oxfmt": "^0.49.0",
|
|
88
|
+
"oxlint": "^1.64.0",
|
|
87
89
|
"typescript": "^6.0.3",
|
|
88
|
-
"vitest": "^4.1.
|
|
90
|
+
"vitest": "^4.1.6"
|
|
89
91
|
},
|
|
90
92
|
"engines": {
|
|
91
93
|
"node": ">=25.8.1 <27"
|
package/playwright.config.ts
CHANGED
|
Binary file
|
package/public/favicon.ico
CHANGED
|
Binary file
|
package/public/logo192.png
CHANGED
|
Binary file
|
package/public/logo512.png
CHANGED
|
Binary file
|
package/public/manifest.json
CHANGED