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 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 X memory across archives, DMs, likes, bookmarks, and moderation.
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 X user ids, force OAuth2 for `xurl` collection reads, and tolerate large/current `bird` bookmark payloads.
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 X web cookie fallback for block and unblock actions when the X API rejects OAuth2 block writes.
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 X memory in SQLite: archives, DMs, likes, bookmarks
1
+ # birdclaw 🪶 — Local Twitter memory in SQLite: archives, DMs, likes, bookmarks
2
2
 
3
- `birdclaw` is a local-first X workspace: archive import, cached live reads, focused triage, and reply flows in one local web app + CLI. Built by [@steipete](https://github.com/steipete/).
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 X data in local SQLite
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 X metadata
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 X web cookie session when OAuth2 block writes are rejected
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 X timeline
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
- - X still rejects pure OAuth2 block writes, so `auto` is the safe default for block/unblock
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 X metadata
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.0",
4
- "description": "Local X memory in SQLite for archives, DMs, likes, bookmarks, and moderation",
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 X workspace")
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 X/Twitter archives on disk")
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 an X/Twitter archive into the local SQLite store")
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 X metadata")
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 X collections into the local store");
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 X.</h1>
32
+ <h1 className={brandMarkClass}>Quiet signal for Twitter.</h1>
33
33
  </div>
34
34
  <div className={navLinksClass}>
35
35
  {links.map((link) => {
@@ -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 X user id for @${resolvedAccount.username}`,
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 X 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.",
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 X user id: ${query}`);
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 X user id for @${username}`);
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 X user id for @${username}`);
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 X user id");
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 X user id for @${username}`);
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 X user id");
385
+ throw new Error("Could not resolve authenticated Twitter user id");
386
386
  }
387
387
  resolvedUserId = String(user.id);
388
388
  }
@@ -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 X URL"
243
+ placeholder="Handle, name, bio, or Twitter URL"
244
244
  value={search}
245
245
  />
246
246
  <button