birdclaw 0.2.0 → 0.2.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 +9 -3
- package/README.md +8 -8
- package/package.json +2 -2
- package/src/cli.ts +5 -5
- package/src/components/AppNav.tsx +1 -1
- package/src/lib/mentions-live.ts +1 -1
- package/src/lib/openai.ts +1 -1
- package/src/lib/profile-replies.ts +1 -1
- package/src/lib/timeline-collections-live.ts +1 -1
- package/src/lib/xurl.ts +4 -4
- package/src/routes/blocks.tsx +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.2.1 - 2026-04-27
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Use Twitter wording in public descriptions, docs, CLI help, and release notes.
|
|
10
|
+
|
|
5
11
|
## 0.2.0 - 2026-04-27
|
|
6
12
|
|
|
7
13
|
### Added
|
|
@@ -13,7 +19,7 @@ All notable changes to this project will be documented in this file.
|
|
|
13
19
|
|
|
14
20
|
### Changed
|
|
15
21
|
|
|
16
|
-
- Update the README tagline and package description for local
|
|
22
|
+
- Update the README tagline and package description for local Twitter memory across archives, DMs, likes, bookmarks, and moderation.
|
|
17
23
|
- Refresh dependencies, including `jsdom` 29.1.0.
|
|
18
24
|
- Hide reply state and reply actions in saved likes/bookmarks web lanes.
|
|
19
25
|
- Shard backup DMs by year and route unknown tweet dates to `data/tweets/unknown.jsonl` so Git backups stay compact and avoid bogus 1970 files.
|
|
@@ -21,7 +27,7 @@ All notable changes to this project will be documented in this file.
|
|
|
21
27
|
|
|
22
28
|
### Fixed
|
|
23
29
|
|
|
24
|
-
- Fix live bookmark sync to use stored
|
|
30
|
+
- Fix live bookmark sync to use stored Twitter user ids, force OAuth2 for `xurl` collection reads, and tolerate large/current `bird` bookmark payloads.
|
|
25
31
|
- Fix fresh-machine backup sync so demo data is never exported into Git backups, and keep no-op syncs from creating metadata-only commits.
|
|
26
32
|
|
|
27
33
|
## 0.1.1 - 2026-04-27
|
|
@@ -38,7 +44,7 @@ All notable changes to this project will be documented in this file.
|
|
|
38
44
|
|
|
39
45
|
### Added
|
|
40
46
|
|
|
41
|
-
- Add
|
|
47
|
+
- Add Twitter web cookie fallback for block and unblock actions when the Twitter API rejects OAuth2 block writes.
|
|
42
48
|
- Add `profiles replies` so moderation triage can inspect a user's recent reply pattern before blocking.
|
|
43
49
|
- Add `blocks import <path>` for one-shot blocklist application from a file.
|
|
44
50
|
- Add paged `mentions export --mode xurl --all --max-pages <n>` so moderation loops can scan the full retrievable mentions window.
|
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
# birdclaw 🪶 — Local
|
|
1
|
+
# birdclaw 🪶 — Local Twitter memory in SQLite: archives, DMs, likes, bookmarks
|
|
2
2
|
|
|
3
|
-
`birdclaw` is a local-first
|
|
3
|
+
`birdclaw` is a local-first Twitter workspace: archive import, cached live reads, focused triage, and reply flows in one local web app + CLI. Built by [@steipete](https://github.com/steipete/).
|
|
4
4
|
|
|
5
5
|
Status: WIP. Real and usable. Not done. Expect schema churn, transport gaps, and rough edges while the core settles.
|
|
6
6
|
|
|
7
7
|
## What It Does
|
|
8
8
|
|
|
9
|
-
- keeps your
|
|
9
|
+
- keeps your Twitter data in local SQLite
|
|
10
10
|
- stores media and avatar cache under `~/.birdclaw`
|
|
11
11
|
- imports archives when you have them
|
|
12
12
|
- still works when you do not
|
|
@@ -24,7 +24,7 @@ Status: WIP. Real and usable. Not done. Expect schema churn, transport gaps, and
|
|
|
24
24
|
- archive import for bookmark exports when present
|
|
25
25
|
- live likes and bookmarks sync through `xurl` or `bird`
|
|
26
26
|
- Git-friendly text backups with yearly tweet shards and per-conversation DM shards
|
|
27
|
-
- profile hydration from live
|
|
27
|
+
- profile hydration from live Twitter metadata
|
|
28
28
|
- local avatar cache
|
|
29
29
|
- local media cache root under `~/.birdclaw`
|
|
30
30
|
|
|
@@ -61,7 +61,7 @@ Status: WIP. Real and usable. Not done. Expect schema churn, transport gaps, and
|
|
|
61
61
|
- import batch blocklists in one call
|
|
62
62
|
- add / remove local mutes
|
|
63
63
|
- sync remote blocks through `xurl` when available
|
|
64
|
-
- fall back to the
|
|
64
|
+
- fall back to the Twitter web cookie session when OAuth2 block writes are rejected
|
|
65
65
|
|
|
66
66
|
### Safety
|
|
67
67
|
|
|
@@ -82,7 +82,7 @@ If you need polished product-grade sync parity today, this is not there yet.
|
|
|
82
82
|
|
|
83
83
|
## Screens
|
|
84
84
|
|
|
85
|
-
- `Home`: read and reply without fighting the main
|
|
85
|
+
- `Home`: read and reply without fighting the main Twitter timeline
|
|
86
86
|
- `Mentions`: work the reply queue with clean filters
|
|
87
87
|
- `Likes` / `Bookmarks`: revisit saved posts from archive or live sync
|
|
88
88
|
- `DMs`: triage by sender context, follower count, and influence
|
|
@@ -295,7 +295,7 @@ Notes:
|
|
|
295
295
|
- `ban` / `unban` accept `--transport auto|bird|xurl`
|
|
296
296
|
- `auto` tries `bird` first, then falls back to `xurl` when bird fails
|
|
297
297
|
- forced `xurl` writes still verify through `bird status` before sqlite changes
|
|
298
|
-
-
|
|
298
|
+
- Twitter still rejects pure OAuth2 block writes, so `auto` is the safe default for block/unblock
|
|
299
299
|
- `blocks import` accepts newline-delimited blocklists with comments and markdown bullets
|
|
300
300
|
- `blocks sync` is for slow/manual remote reconciliation; not for a hot cron loop
|
|
301
301
|
- `blocks record` stores a known-good remote block locally without issuing another live write
|
|
@@ -432,7 +432,7 @@ tail -n 1 ~/.birdclaw/audit/bookmarks-sync.jsonl | jq .
|
|
|
432
432
|
## Typical Workflow
|
|
433
433
|
|
|
434
434
|
1. import your archive if you have one
|
|
435
|
-
2. hydrate imported profiles from live
|
|
435
|
+
2. hydrate imported profiles from live Twitter metadata
|
|
436
436
|
3. use `Home` for reading
|
|
437
437
|
4. use `Mentions` for reply triage
|
|
438
438
|
5. when one account feels borderline, inspect `profiles replies`
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "birdclaw",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Local
|
|
3
|
+
"version": "0.2.1",
|
|
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",
|
|
7
7
|
"repository": {
|
package/src/cli.ts
CHANGED
|
@@ -85,7 +85,7 @@ async function autoSyncAfterWrite() {
|
|
|
85
85
|
|
|
86
86
|
program
|
|
87
87
|
.name("birdclaw")
|
|
88
|
-
.description("Local-first
|
|
88
|
+
.description("Local-first Twitter workspace")
|
|
89
89
|
.version(packageVersion.version ?? "0.0.0")
|
|
90
90
|
.option("--json", "Emit JSON output");
|
|
91
91
|
|
|
@@ -118,7 +118,7 @@ program
|
|
|
118
118
|
|
|
119
119
|
program
|
|
120
120
|
.command("archive find")
|
|
121
|
-
.description("Find likely
|
|
121
|
+
.description("Find likely Twitter archives on disk")
|
|
122
122
|
.action(async () => {
|
|
123
123
|
const items = await findArchives();
|
|
124
124
|
print(items, program.opts().json ?? false);
|
|
@@ -130,7 +130,7 @@ const importCommand = program
|
|
|
130
130
|
|
|
131
131
|
importCommand
|
|
132
132
|
.command("archive [archivePath]")
|
|
133
|
-
.description("Import
|
|
133
|
+
.description("Import a Twitter archive into the local SQLite store")
|
|
134
134
|
.action(async (archivePath) => {
|
|
135
135
|
let resolvedArchivePath = archivePath;
|
|
136
136
|
if (!resolvedArchivePath) {
|
|
@@ -151,7 +151,7 @@ importCommand
|
|
|
151
151
|
|
|
152
152
|
importCommand
|
|
153
153
|
.command("hydrate-profiles")
|
|
154
|
-
.description("Backfill archive-imported profiles from live
|
|
154
|
+
.description("Backfill archive-imported profiles from live Twitter metadata")
|
|
155
155
|
.action(async () => {
|
|
156
156
|
const result = await hydrateProfilesFromX();
|
|
157
157
|
await autoSyncAfterWrite();
|
|
@@ -323,7 +323,7 @@ const dmsCommand = program.command("dms").description("Direct messages");
|
|
|
323
323
|
|
|
324
324
|
const syncCommand = program
|
|
325
325
|
.command("sync")
|
|
326
|
-
.description("Refresh live
|
|
326
|
+
.description("Refresh live Twitter collections into the local store");
|
|
327
327
|
|
|
328
328
|
for (const kind of ["likes", "bookmarks"] as const) {
|
|
329
329
|
syncCommand
|
|
@@ -29,7 +29,7 @@ export function AppNav() {
|
|
|
29
29
|
<nav className={navClass}>
|
|
30
30
|
<div>
|
|
31
31
|
<p className={eyebrowClass}>birdclaw</p>
|
|
32
|
-
<h1 className={brandMarkClass}>Quiet signal for
|
|
32
|
+
<h1 className={brandMarkClass}>Quiet signal for Twitter.</h1>
|
|
33
33
|
</div>
|
|
34
34
|
<div className={navLinksClass}>
|
|
35
35
|
{links.map((link) => {
|
package/src/lib/mentions-live.ts
CHANGED
|
@@ -307,7 +307,7 @@ async function fetchMentionsViaXurl({
|
|
|
307
307
|
const [accountUser] = await lookupUsersByHandles([resolvedAccount.username]);
|
|
308
308
|
if (!accountUser?.id) {
|
|
309
309
|
throw new Error(
|
|
310
|
-
`Could not resolve
|
|
310
|
+
`Could not resolve Twitter user id for @${resolvedAccount.username}`,
|
|
311
311
|
);
|
|
312
312
|
}
|
|
313
313
|
|
package/src/lib/openai.ts
CHANGED
|
@@ -44,7 +44,7 @@ export async function scoreInboxItemWithOpenAI(
|
|
|
44
44
|
{
|
|
45
45
|
role: "system",
|
|
46
46
|
content:
|
|
47
|
-
"You rank inbound
|
|
47
|
+
"You rank inbound Twitter mentions and DMs for Peter Steinberger. Return JSON only with keys score, summary, reasoning. Score 0-100. High score means worth replying soon. Prefer specific, actionable, novel, high-signal items. Penalize generic praise, low-context asks, and low-signal chatter. summary max 18 words. reasoning max 28 words.",
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
role: "user",
|
|
@@ -16,7 +16,7 @@ export async function inspectProfileReplies(
|
|
|
16
16
|
): Promise<ProfileRepliesResponse> {
|
|
17
17
|
const resolved = await resolveProfile(query);
|
|
18
18
|
if (!resolved.externalUserId) {
|
|
19
|
-
throw new Error(`Profile has no external
|
|
19
|
+
throw new Error(`Profile has no external Twitter user id: ${query}`);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
const timeline = await listUserTweets(resolved.externalUserId, {
|
|
@@ -248,7 +248,7 @@ async function fetchXurlCollection({
|
|
|
248
248
|
if (!resolvedUserId) {
|
|
249
249
|
const [accountUser] = await lookupUsersByHandles([username]);
|
|
250
250
|
if (!accountUser?.id) {
|
|
251
|
-
throw new Error(`Could not resolve
|
|
251
|
+
throw new Error(`Could not resolve Twitter user id for @${username}`);
|
|
252
252
|
}
|
|
253
253
|
resolvedUserId = String(accountUser.id);
|
|
254
254
|
}
|
package/src/lib/xurl.ts
CHANGED
|
@@ -317,13 +317,13 @@ export async function listMentionsViaXurl({
|
|
|
317
317
|
if (username) {
|
|
318
318
|
const [user] = await lookupUsersByHandles([username]);
|
|
319
319
|
if (!user?.id) {
|
|
320
|
-
throw new Error(`Could not resolve
|
|
320
|
+
throw new Error(`Could not resolve Twitter user id for @${username}`);
|
|
321
321
|
}
|
|
322
322
|
resolvedUserId = String(user.id);
|
|
323
323
|
} else {
|
|
324
324
|
const user = await lookupAuthenticatedUser();
|
|
325
325
|
if (!user?.id) {
|
|
326
|
-
throw new Error("Could not resolve authenticated
|
|
326
|
+
throw new Error("Could not resolve authenticated Twitter user id");
|
|
327
327
|
}
|
|
328
328
|
resolvedUserId = String(user.id);
|
|
329
329
|
}
|
|
@@ -376,13 +376,13 @@ async function listTimelineCollectionViaXurl({
|
|
|
376
376
|
if (username) {
|
|
377
377
|
const [user] = await lookupUsersByHandles([username]);
|
|
378
378
|
if (!user?.id) {
|
|
379
|
-
throw new Error(`Could not resolve
|
|
379
|
+
throw new Error(`Could not resolve Twitter user id for @${username}`);
|
|
380
380
|
}
|
|
381
381
|
resolvedUserId = String(user.id);
|
|
382
382
|
} else {
|
|
383
383
|
const user = await lookupAuthenticatedUser();
|
|
384
384
|
if (!user?.id) {
|
|
385
|
-
throw new Error("Could not resolve authenticated
|
|
385
|
+
throw new Error("Could not resolve authenticated Twitter user id");
|
|
386
386
|
}
|
|
387
387
|
resolvedUserId = String(user.id);
|
|
388
388
|
}
|
package/src/routes/blocks.tsx
CHANGED
|
@@ -240,7 +240,7 @@ function BlocksRoute() {
|
|
|
240
240
|
className={cx(textFieldClass, textFieldWideClass)}
|
|
241
241
|
disabled={!hasAccountId}
|
|
242
242
|
onChange={(event) => setSearch(event.target.value)}
|
|
243
|
-
placeholder="Handle, name, bio, or
|
|
243
|
+
placeholder="Handle, name, bio, or Twitter URL"
|
|
244
244
|
value={search}
|
|
245
245
|
/>
|
|
246
246
|
<button
|