birdclaw 0.3.0 → 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 +19 -0
- package/README.md +37 -9
- package/package.json +4 -6
- package/src/cli.ts +176 -20
- package/src/lib/archive-import.ts +25 -0
- package/src/lib/backup.ts +269 -33
- package/src/lib/bird-actions.ts +17 -0
- package/src/lib/bird.ts +189 -0
- package/src/lib/blocks.ts +3 -3
- package/src/lib/bookmark-sync-job.ts +3 -3
- package/src/lib/db.ts +313 -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 +5 -9
- package/src/lib/mentions-live.ts +18 -7
- package/src/lib/moderation-target.ts +4 -4
- 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 +4 -28
- package/src/lib/profile-resolver.ts +428 -0
- package/src/lib/queries.ts +391 -57
- package/src/lib/research.ts +11 -0
- package/src/lib/seed.ts +2 -2
- package/src/lib/sqlite.ts +143 -0
- package/src/lib/timeline-collections-live.ts +7 -7
- package/src/lib/timeline-live.ts +16 -9
- package/src/lib/tweet-account-edges.ts +39 -0
- package/src/lib/types.ts +74 -0
- package/src/lib/url-expansion.ts +147 -0
- package/src/lib/whois.ts +1060 -0
- package/src/lib/x-profile.ts +140 -12
- package/src/lib/xurl.ts +7 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.4.0 - 2026-05-09
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Preserve richer X profile metadata during `bird`/`xurl` profile hydration, including profile URLs, profile URL entities, locations, verification type, raw profile JSON, and X affiliation/highlighted-label metadata.
|
|
10
|
+
- Add first-class `profile_affiliations` storage, backup/export/import support, and `whois --json` `profileEvidence` so identity lookups can explain whether a match came from bio text, profile URLs, affiliation badges, DM context, or expanded links.
|
|
11
|
+
- Add profile-change snapshots for hydrated profiles, preserving prior bio/profile URL/location/verification/affiliation states so identity searches can surface current and previous affiliation evidence.
|
|
12
|
+
- Add first-class bio entity extraction for profile bios and profile URLs, including `@handle`, domain, and company-phrase evidence used by fuzzy identity searches such as `whois "blacksmith guy"`.
|
|
13
|
+
- Add a derived `identity_search_index` and `whois` filters for affiliation-oriented identity lookups: `--affiliation`, `--current-affiliation`, and `--exclude-domain-only`.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Use Node's native `node:sqlite` runtime instead of `better-sqlite3`, removing the native npm dependency while preserving the existing synchronous SQLite API surface.
|
|
18
|
+
- Allow Node 26.x in the package engine range and update install docs for the native SQLite runtime.
|
|
19
|
+
- Improve DM `whois` ranking with Sweetistics-style profile evidence scoring: profile URLs and affiliation badges now boost relevant candidates, while cached profile and URL lookups still avoid repeated API/network work.
|
|
20
|
+
- Resolve synthetic X highlighted-label organization badges into real local organization profile ids when `bird` can hydrate the org handle.
|
|
21
|
+
- Rank current affiliation and bio identity evidence above plain profile domains in `whois`, group human output into ambiguity buckets, and explain "why this person?" with the strongest typed evidence first.
|
|
22
|
+
- Use `bird profiles --json` for batch profile hydration when available, falling back to single-profile `bird user --profile-only --json`.
|
|
23
|
+
|
|
5
24
|
## 0.3.0 - 2026-05-05
|
|
6
25
|
|
|
7
26
|
### Added
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ Status: WIP. Real and usable. Not done. Expect schema churn, transport gaps, and
|
|
|
17
17
|
|
|
18
18
|
### Local data + storage
|
|
19
19
|
|
|
20
|
-
- one shared SQLite DB for multiple accounts
|
|
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
23
|
- archive import for tweets, likes, profiles, and full DMs
|
|
@@ -25,6 +25,7 @@ Status: WIP. Real and usable. Not done. Expect schema churn, transport gaps, and
|
|
|
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
27
|
- profile hydration from live Twitter metadata
|
|
28
|
+
- profile-change history, affiliation badge edges, and extracted bio entities for local identity lookups
|
|
28
29
|
- local avatar cache
|
|
29
30
|
- local media cache root under `~/.birdclaw`
|
|
30
31
|
|
|
@@ -112,7 +113,7 @@ export BIRDCLAW_HOME=/path/to/custom/root
|
|
|
112
113
|
|
|
113
114
|
## Requirements
|
|
114
115
|
|
|
115
|
-
- Node `25.8.1`
|
|
116
|
+
- Node `25.8.1` or Node 26.x
|
|
116
117
|
- `pnpm`
|
|
117
118
|
- macOS recommended for Spotlight archive discovery
|
|
118
119
|
- `xurl` optional for live reads / writes
|
|
@@ -259,7 +260,7 @@ Notes:
|
|
|
259
260
|
- `actions.transport` accepts `auto`, `bird`, or `xurl`
|
|
260
261
|
- `bird` mode uses your local `bird` CLI and caches its mentions output into birdclaw's canonical store
|
|
261
262
|
- filters still work in `xurl` mode; filtered payloads are rebuilt from the local canonical store after sync
|
|
262
|
-
- `sync likes`, `sync bookmarks`, `sync timeline`, and `sync mention-threads` store live results in the
|
|
263
|
+
- `sync likes`, `sync bookmarks`, `sync timeline`, and `sync mention-threads` store live results in the canonical local store; per-account home/mention/like/bookmark membership is kept as edges so shared tweets do not clobber account ownership
|
|
263
264
|
|
|
264
265
|
### Research bookmarks and threads
|
|
265
266
|
|
|
@@ -275,11 +276,33 @@ birdclaw research --account acct_primary --out ~/research/codex.md
|
|
|
275
276
|
```bash
|
|
276
277
|
pnpm cli search dms "prototype" --json
|
|
277
278
|
pnpm cli search dms "layout" --min-followers 1000 --min-influence-score 120 --sort influence --json
|
|
279
|
+
pnpm cli search dms "blacksmith" --context 4 --resolve-profiles --expand-urls --no-xurl-fallback --json
|
|
280
|
+
pnpm cli whois "blacksmith guy" --context 4 --no-xurl-fallback --json
|
|
281
|
+
pnpm cli whois "github guy" --current-affiliation github --exclude-domain-only --no-xurl-fallback
|
|
282
|
+
pnpm cli whois "blacksmith" --tweets --context 4 --no-xurl-fallback --json
|
|
278
283
|
pnpm cli dms sync --limit 50 --refresh --json
|
|
279
284
|
pnpm cli dms list --refresh --limit 10 --json
|
|
280
285
|
pnpm cli dms list --unreplied --min-followers 500 --min-influence-score 90 --sort influence --json
|
|
281
286
|
```
|
|
282
287
|
|
|
288
|
+
`--resolve-profiles` fills archive-imported numeric DM profiles through the local
|
|
289
|
+
cache first, then `bird`, then `xurl` unless `--no-xurl-fallback` is set.
|
|
290
|
+
Resolved profiles keep bio, location, profile URL, verification type, structured
|
|
291
|
+
URL entities, raw profile JSON, and any X affiliation badge metadata Birdclaw can
|
|
292
|
+
see. When a highlighted-label badge only gives a synthetic label plus handle,
|
|
293
|
+
Birdclaw tries to hydrate that org handle through `bird` and rewrites the edge to
|
|
294
|
+
the real local organization profile id. Profile changes are snapshotted over
|
|
295
|
+
time, and bios are indexed for `@handles`, domains, and company phrases.
|
|
296
|
+
`whois` uses that profile context plus DM context and cached URL expansion to
|
|
297
|
+
return typed evidence such as `profile_bio`, `profile_url`, `profile_bio_url`,
|
|
298
|
+
`affiliation`, `bio_handle`, `bio_domain`, `bio_company`, `profile_history`,
|
|
299
|
+
`dm_context`, and `expanded_url`. It keeps a derived `identity_search_index`
|
|
300
|
+
for fast local profile-evidence lookups, ranks current affiliation and bio
|
|
301
|
+
identity evidence above plain domains, and groups human output into likely
|
|
302
|
+
affiliated, ecosystem, link-only, and DM-context buckets. Use
|
|
303
|
+
`--affiliation`, `--current-affiliation`, and `--exclude-domain-only` when you
|
|
304
|
+
want "GitHub people" rather than anyone with a `github.com` link.
|
|
305
|
+
|
|
283
306
|
### AI inbox
|
|
284
307
|
|
|
285
308
|
```bash
|
|
@@ -377,8 +400,13 @@ Layout:
|
|
|
377
400
|
manifest.json
|
|
378
401
|
data/accounts.jsonl
|
|
379
402
|
data/profiles.jsonl
|
|
403
|
+
data/profile_affiliations.jsonl
|
|
404
|
+
data/profile_snapshots.jsonl
|
|
405
|
+
data/profile_bio_entities.jsonl
|
|
380
406
|
data/tweets/YYYY.jsonl
|
|
381
407
|
data/tweets/unknown.jsonl
|
|
408
|
+
data/timeline_edges/home.jsonl
|
|
409
|
+
data/timeline_edges/mention.jsonl
|
|
382
410
|
data/collections/likes.jsonl
|
|
383
411
|
data/collections/bookmarks.jsonl
|
|
384
412
|
data/dms/conversations.jsonl
|
|
@@ -387,7 +415,7 @@ data/moderation/blocks.jsonl
|
|
|
387
415
|
data/moderation/mutes.jsonl
|
|
388
416
|
```
|
|
389
417
|
|
|
390
|
-
Tweets are sharded by year for human browsing and yearly analysis. Collection-only tweets whose real creation date is unknown go into `data/tweets/unknown.jsonl` instead of pretending they belong to 1970.
|
|
418
|
+
Tweets are sharded by year for human browsing and yearly analysis. Collection-only tweets whose real creation date is unknown go into `data/tweets/unknown.jsonl` instead of pretending they belong to 1970. Timeline membership is stored in `data/timeline_edges`; likes and bookmarks are stored as account-scoped collection edges in `data/collections`. DMs are sharded by year with `conversation_id` in each row; this keeps Git fast while preserving conversation membership.
|
|
391
419
|
|
|
392
420
|
Use `backup sync` when the target is a private Git repo. It pulls first, merge-imports the remote backup into local SQLite, exports the local union back into text shards, commits, and pushes.
|
|
393
421
|
|
|
@@ -477,11 +505,11 @@ pnpm cli auth status --json
|
|
|
477
505
|
## Testing
|
|
478
506
|
|
|
479
507
|
```bash
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
508
|
+
pnpm check
|
|
509
|
+
pnpm test
|
|
510
|
+
pnpm coverage
|
|
511
|
+
pnpm build
|
|
512
|
+
pnpm e2e
|
|
485
513
|
```
|
|
486
514
|
|
|
487
515
|
Current bar:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "birdclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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",
|
|
@@ -46,7 +46,8 @@
|
|
|
46
46
|
"format:check": "oxfmt --check src scripts playwright vite.config.ts vitest.config.ts playwright.config.ts",
|
|
47
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",
|
|
48
48
|
"check": "pnpm run format:check && pnpm run lint",
|
|
49
|
-
"cli": "tsx src/cli.ts"
|
|
49
|
+
"cli": "tsx src/cli.ts",
|
|
50
|
+
"docs:site": "node scripts/build-docs-site.mjs"
|
|
50
51
|
},
|
|
51
52
|
"dependencies": {
|
|
52
53
|
"@steipete/sweet-cookie": "^0.2.0",
|
|
@@ -59,7 +60,6 @@
|
|
|
59
60
|
"@tanstack/react-start": "1.167.61",
|
|
60
61
|
"@tanstack/router-plugin": "^1.167.32",
|
|
61
62
|
"@vitejs/plugin-react": "^6.0.1",
|
|
62
|
-
"better-sqlite3": "^12.9.0",
|
|
63
63
|
"commander": "^14.0.3",
|
|
64
64
|
"kysely": "^0.28.17",
|
|
65
65
|
"lucide-react": "^1.14.0",
|
|
@@ -76,7 +76,6 @@
|
|
|
76
76
|
"@testing-library/dom": "^10.4.1",
|
|
77
77
|
"@testing-library/jest-dom": "^6.9.1",
|
|
78
78
|
"@testing-library/react": "^16.3.2",
|
|
79
|
-
"@types/better-sqlite3": "^7.6.13",
|
|
80
79
|
"@types/node": "^25.6.0",
|
|
81
80
|
"@types/react": "^19.2.14",
|
|
82
81
|
"@types/react-dom": "^19.2.3",
|
|
@@ -89,12 +88,11 @@
|
|
|
89
88
|
"vitest": "^4.1.5"
|
|
90
89
|
},
|
|
91
90
|
"engines": {
|
|
92
|
-
"node": ">=25.8.1 <
|
|
91
|
+
"node": ">=25.8.1 <27"
|
|
93
92
|
},
|
|
94
93
|
"packageManager": "pnpm@10.31.0",
|
|
95
94
|
"pnpm": {
|
|
96
95
|
"onlyBuiltDependencies": [
|
|
97
|
-
"better-sqlite3",
|
|
98
96
|
"esbuild",
|
|
99
97
|
"lightningcss"
|
|
100
98
|
]
|
package/src/cli.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
exportMentionsViaCachedXurl,
|
|
37
37
|
} from "#/lib/mentions-live";
|
|
38
38
|
import { hydrateProfilesFromX } from "#/lib/profile-hydration";
|
|
39
|
+
import { resolveProfilesForIds } from "#/lib/profile-resolver";
|
|
39
40
|
import { inspectProfileReplies } from "#/lib/profile-replies";
|
|
40
41
|
import { runResearchMode } from "#/lib/research";
|
|
41
42
|
import {
|
|
@@ -51,6 +52,8 @@ import {
|
|
|
51
52
|
type TimelineCollectionMode,
|
|
52
53
|
} from "#/lib/timeline-collections-live";
|
|
53
54
|
import { syncHomeTimeline } from "#/lib/timeline-live";
|
|
55
|
+
import { expandUrlsFromTexts } from "#/lib/url-expansion";
|
|
56
|
+
import { formatWhois, runWhois } from "#/lib/whois";
|
|
54
57
|
|
|
55
58
|
const program = new Command();
|
|
56
59
|
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
@@ -101,6 +104,56 @@ function resolveActionOptions(options: { transport?: string }) {
|
|
|
101
104
|
};
|
|
102
105
|
}
|
|
103
106
|
|
|
107
|
+
async function enrichDmItems(
|
|
108
|
+
query: Parameters<typeof listDmConversations>[0],
|
|
109
|
+
options: {
|
|
110
|
+
resolveProfiles?: boolean;
|
|
111
|
+
expandUrls?: boolean;
|
|
112
|
+
refreshProfileCache?: boolean;
|
|
113
|
+
refreshUrlCache?: boolean;
|
|
114
|
+
xurlFallback?: boolean;
|
|
115
|
+
},
|
|
116
|
+
) {
|
|
117
|
+
let items = listDmConversations(query);
|
|
118
|
+
const profileResolution = options.resolveProfiles
|
|
119
|
+
? await resolveProfilesForIds(
|
|
120
|
+
items.map((item) => item.participant.id),
|
|
121
|
+
{
|
|
122
|
+
refresh: options.refreshProfileCache,
|
|
123
|
+
xurlFallback: options.xurlFallback ?? true,
|
|
124
|
+
},
|
|
125
|
+
)
|
|
126
|
+
: undefined;
|
|
127
|
+
if (profileResolution) {
|
|
128
|
+
items = listDmConversations(query);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const urlExpansions = options.expandUrls
|
|
132
|
+
? await expandUrlsFromTexts(
|
|
133
|
+
items.flatMap((item) => [
|
|
134
|
+
item.lastMessagePreview,
|
|
135
|
+
item.searchSnippet ?? "",
|
|
136
|
+
...(item.matches ?? []).flatMap((match) => [
|
|
137
|
+
...match.before.map((message) => message.text),
|
|
138
|
+
match.message.text,
|
|
139
|
+
...match.after.map((message) => message.text),
|
|
140
|
+
]),
|
|
141
|
+
]),
|
|
142
|
+
{ refresh: options.refreshUrlCache },
|
|
143
|
+
)
|
|
144
|
+
: undefined;
|
|
145
|
+
|
|
146
|
+
if (!profileResolution && !urlExpansions) {
|
|
147
|
+
return items;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
items,
|
|
152
|
+
...(profileResolution ? { profileResolution } : {}),
|
|
153
|
+
...(urlExpansions ? { urlExpansions } : {}),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
104
157
|
async function autoUpdateBeforeRead() {
|
|
105
158
|
const result = await maybeAutoUpdateBackup();
|
|
106
159
|
if (!result.ok) {
|
|
@@ -251,17 +304,37 @@ searchCommand
|
|
|
251
304
|
.option("--min-influence-score <n>", "Minimum derived influence score")
|
|
252
305
|
.option("--max-influence-score <n>", "Maximum derived influence score")
|
|
253
306
|
.option("--sort <mode>", "recent or influence", "recent")
|
|
307
|
+
.option(
|
|
308
|
+
"--context <n>",
|
|
309
|
+
"Include N messages before and after each match",
|
|
310
|
+
"0",
|
|
311
|
+
)
|
|
312
|
+
.option(
|
|
313
|
+
"--resolve-profiles",
|
|
314
|
+
"Resolve placeholder DM profiles through cache/bird/xurl",
|
|
315
|
+
)
|
|
316
|
+
.option("--expand-urls", "Expand URLs through the persistent URL cache")
|
|
317
|
+
.option("--refresh-profile-cache", "Bypass profile lookup cache")
|
|
318
|
+
.option("--refresh-url-cache", "Bypass URL expansion cache")
|
|
319
|
+
.option(
|
|
320
|
+
"--no-xurl-fallback",
|
|
321
|
+
"Do not fall back to xurl after bird profile lookup",
|
|
322
|
+
)
|
|
254
323
|
.option("--replied", "Only replied threads")
|
|
255
324
|
.option("--unreplied", "Only unreplied threads")
|
|
256
325
|
.option("--limit <n>", "Limit results", "20")
|
|
257
326
|
.action(async (query, options) => {
|
|
258
327
|
await autoUpdateBeforeRead();
|
|
328
|
+
const context = parseNonNegativeIntegerOption(options.context, "--context");
|
|
329
|
+
if (context === undefined) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
259
332
|
const replyFilter = options.replied
|
|
260
333
|
? "replied"
|
|
261
334
|
: options.unreplied
|
|
262
335
|
? "unreplied"
|
|
263
336
|
: "all";
|
|
264
|
-
const
|
|
337
|
+
const dmQuery = {
|
|
265
338
|
search: query,
|
|
266
339
|
participant: options.participant,
|
|
267
340
|
minFollowers: options.minFollowers
|
|
@@ -278,11 +351,74 @@ searchCommand
|
|
|
278
351
|
: undefined,
|
|
279
352
|
sort: options.sort === "influence" ? "influence" : "recent",
|
|
280
353
|
replyFilter,
|
|
354
|
+
context,
|
|
281
355
|
limit: Number(options.limit),
|
|
356
|
+
} as const;
|
|
357
|
+
const items = await enrichDmItems(dmQuery, {
|
|
358
|
+
resolveProfiles: Boolean(options.resolveProfiles),
|
|
359
|
+
expandUrls: Boolean(options.expandUrls),
|
|
360
|
+
refreshProfileCache: Boolean(options.refreshProfileCache),
|
|
361
|
+
refreshUrlCache: Boolean(options.refreshUrlCache),
|
|
362
|
+
xurlFallback: options.xurlFallback,
|
|
282
363
|
});
|
|
283
364
|
print(items, program.opts().json ?? false);
|
|
284
365
|
});
|
|
285
366
|
|
|
367
|
+
program
|
|
368
|
+
.command("whois <query>")
|
|
369
|
+
.description("Identify likely people or orgs from local DMs and tweets")
|
|
370
|
+
.option("--account <accountId>", "Account id")
|
|
371
|
+
.option("--no-dms", "Do not search DMs")
|
|
372
|
+
.option("--tweets", "Include local tweet search evidence")
|
|
373
|
+
.option("--no-resolve-profiles", "Do not resolve placeholder profiles")
|
|
374
|
+
.option("--no-expand-urls", "Do not expand URLs")
|
|
375
|
+
.option("--refresh-profile-cache", "Bypass profile lookup cache")
|
|
376
|
+
.option("--refresh-url-cache", "Bypass URL expansion cache")
|
|
377
|
+
.option(
|
|
378
|
+
"--no-xurl-fallback",
|
|
379
|
+
"Do not fall back to xurl after bird profile lookup",
|
|
380
|
+
)
|
|
381
|
+
.option(
|
|
382
|
+
"--affiliation <query>",
|
|
383
|
+
"Require affiliation, bio, or history evidence",
|
|
384
|
+
)
|
|
385
|
+
.option(
|
|
386
|
+
"--current-affiliation <query>",
|
|
387
|
+
"Require an active affiliation badge",
|
|
388
|
+
)
|
|
389
|
+
.option(
|
|
390
|
+
"--exclude-domain-only",
|
|
391
|
+
"Drop candidates that only match domains/URLs",
|
|
392
|
+
)
|
|
393
|
+
.option("--context <n>", "DM messages before and after each match", "4")
|
|
394
|
+
.option("--limit <n>", "Limit candidates", "10")
|
|
395
|
+
.action(async (query, options) => {
|
|
396
|
+
await autoUpdateBeforeRead();
|
|
397
|
+
const context = parseNonNegativeIntegerOption(options.context, "--context");
|
|
398
|
+
if (context === undefined) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const result = await runWhois(query, {
|
|
402
|
+
account: options.account,
|
|
403
|
+
dms: options.dms,
|
|
404
|
+
tweets: Boolean(options.tweets),
|
|
405
|
+
resolveProfiles: options.resolveProfiles,
|
|
406
|
+
expandUrls: options.expandUrls,
|
|
407
|
+
refreshProfileCache: Boolean(options.refreshProfileCache),
|
|
408
|
+
refreshUrlCache: Boolean(options.refreshUrlCache),
|
|
409
|
+
xurlFallback: options.xurlFallback,
|
|
410
|
+
affiliation: options.affiliation,
|
|
411
|
+
currentAffiliation: options.currentAffiliation,
|
|
412
|
+
excludeDomainOnly: Boolean(options.excludeDomainOnly),
|
|
413
|
+
context,
|
|
414
|
+
limit: Number(options.limit),
|
|
415
|
+
});
|
|
416
|
+
print(
|
|
417
|
+
program.opts().json ? result : formatWhois(result),
|
|
418
|
+
program.opts().json ?? false,
|
|
419
|
+
);
|
|
420
|
+
});
|
|
421
|
+
|
|
286
422
|
program
|
|
287
423
|
.command("research [query]")
|
|
288
424
|
.description("Build a markdown research brief from bookmarked threads")
|
|
@@ -547,6 +683,17 @@ dmsCommand
|
|
|
547
683
|
.option("--min-influence-score <n>", "Minimum derived influence score")
|
|
548
684
|
.option("--max-influence-score <n>", "Maximum derived influence score")
|
|
549
685
|
.option("--sort <mode>", "recent or influence", "recent")
|
|
686
|
+
.option(
|
|
687
|
+
"--resolve-profiles",
|
|
688
|
+
"Resolve placeholder DM profiles through cache/bird/xurl",
|
|
689
|
+
)
|
|
690
|
+
.option("--expand-urls", "Expand URLs through the persistent URL cache")
|
|
691
|
+
.option("--refresh-profile-cache", "Bypass profile lookup cache")
|
|
692
|
+
.option("--refresh-url-cache", "Bypass URL expansion cache")
|
|
693
|
+
.option(
|
|
694
|
+
"--no-xurl-fallback",
|
|
695
|
+
"Do not fall back to xurl after bird profile lookup",
|
|
696
|
+
)
|
|
550
697
|
.option("--replied", "Only replied threads")
|
|
551
698
|
.option("--unreplied", "Only unreplied threads")
|
|
552
699
|
.option("--limit <n>", "Limit results", "20")
|
|
@@ -567,25 +714,34 @@ dmsCommand
|
|
|
567
714
|
} else {
|
|
568
715
|
await autoUpdateBeforeRead();
|
|
569
716
|
}
|
|
570
|
-
const items =
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
717
|
+
const items = await enrichDmItems(
|
|
718
|
+
{
|
|
719
|
+
account: options.account,
|
|
720
|
+
participant: options.participant,
|
|
721
|
+
minFollowers: options.minFollowers
|
|
722
|
+
? Number(options.minFollowers)
|
|
723
|
+
: undefined,
|
|
724
|
+
maxFollowers: options.maxFollowers
|
|
725
|
+
? Number(options.maxFollowers)
|
|
726
|
+
: undefined,
|
|
727
|
+
minInfluenceScore: options.minInfluenceScore
|
|
728
|
+
? Number(options.minInfluenceScore)
|
|
729
|
+
: undefined,
|
|
730
|
+
maxInfluenceScore: options.maxInfluenceScore
|
|
731
|
+
? Number(options.maxInfluenceScore)
|
|
732
|
+
: undefined,
|
|
733
|
+
sort: options.sort === "influence" ? "influence" : "recent",
|
|
734
|
+
replyFilter,
|
|
735
|
+
limit: Number(options.limit),
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
resolveProfiles: Boolean(options.resolveProfiles),
|
|
739
|
+
expandUrls: Boolean(options.expandUrls),
|
|
740
|
+
refreshProfileCache: Boolean(options.refreshProfileCache),
|
|
741
|
+
refreshUrlCache: Boolean(options.refreshUrlCache),
|
|
742
|
+
xurlFallback: options.xurlFallback,
|
|
743
|
+
},
|
|
744
|
+
);
|
|
589
745
|
print(items, program.opts().json ?? false);
|
|
590
746
|
});
|
|
591
747
|
|
|
@@ -286,6 +286,7 @@ function clearImportedData() {
|
|
|
286
286
|
db.exec(`
|
|
287
287
|
delete from ai_scores;
|
|
288
288
|
delete from tweet_actions;
|
|
289
|
+
delete from tweet_account_edges;
|
|
289
290
|
delete from tweet_collections;
|
|
290
291
|
delete from dm_fts;
|
|
291
292
|
delete from tweets_fts;
|
|
@@ -776,6 +777,16 @@ export async function importArchive(
|
|
|
776
777
|
const insertTweetFts = db.prepare(
|
|
777
778
|
"insert into tweets_fts (tweet_id, text) values (?, ?)",
|
|
778
779
|
);
|
|
780
|
+
const insertTimelineEdge = db.prepare(`
|
|
781
|
+
insert into tweet_account_edges (
|
|
782
|
+
account_id, tweet_id, kind, first_seen_at, last_seen_at, seen_count, source,
|
|
783
|
+
raw_json, updated_at
|
|
784
|
+
) values (?, ?, ?, ?, ?, 1, 'archive', '{}', ?)
|
|
785
|
+
on conflict(account_id, tweet_id, kind) do update set
|
|
786
|
+
first_seen_at = min(tweet_account_edges.first_seen_at, excluded.first_seen_at),
|
|
787
|
+
last_seen_at = max(tweet_account_edges.last_seen_at, excluded.last_seen_at),
|
|
788
|
+
updated_at = max(tweet_account_edges.updated_at, excluded.updated_at)
|
|
789
|
+
`);
|
|
779
790
|
const insertCollection = db.prepare(`
|
|
780
791
|
insert into tweet_collections (
|
|
781
792
|
account_id, tweet_id, kind, collected_at, source, raw_json, updated_at
|
|
@@ -842,6 +853,16 @@ export async function importArchive(
|
|
|
842
853
|
tweet.mediaJson,
|
|
843
854
|
tweet.quotedTweetId,
|
|
844
855
|
);
|
|
856
|
+
if (tweet.kind === "home") {
|
|
857
|
+
insertTimelineEdge.run(
|
|
858
|
+
"acct_primary",
|
|
859
|
+
tweet.id,
|
|
860
|
+
tweet.kind,
|
|
861
|
+
tweet.createdAt,
|
|
862
|
+
tweet.createdAt,
|
|
863
|
+
new Date().toISOString(),
|
|
864
|
+
);
|
|
865
|
+
}
|
|
845
866
|
insertTweetFts.run(tweet.id, tweet.text);
|
|
846
867
|
}
|
|
847
868
|
|
|
@@ -905,8 +926,11 @@ export async function importArchive(
|
|
|
905
926
|
}
|
|
906
927
|
|
|
907
928
|
export const __test__ = {
|
|
929
|
+
normalizeArchivePath,
|
|
908
930
|
extractArchiveJson,
|
|
909
931
|
parseArchiveArray,
|
|
932
|
+
getFirstEntry,
|
|
933
|
+
getMatchingEntries,
|
|
910
934
|
parseTwitterDate,
|
|
911
935
|
asRecord,
|
|
912
936
|
asArray,
|
|
@@ -915,6 +939,7 @@ export const __test__ = {
|
|
|
915
939
|
getTweetMediaCount,
|
|
916
940
|
extractTweetEntities,
|
|
917
941
|
extractTweetMedia,
|
|
942
|
+
extractCollectionTweet,
|
|
918
943
|
buildAccountPayload,
|
|
919
944
|
inferProfileFromDirectory,
|
|
920
945
|
};
|