返回 last30days-skill
bird-search.mjs
根目录 / skills / last30days / scripts / lib / vendor / bird-search / bird-search.mjs
1 #!/usr/bin/env node
2 /**
3 * bird-search.mjs - Vendored Bird CLI search wrapper for /last30days.
4 * Subset of @steipete/bird v0.8.0 (MIT License, Peter Steinberger).
5 *
6 * Usage:
7 * node bird-search.mjs <query> [--count N] [--json]
8 * node bird-search.mjs --whoami
9 * node bird-search.mjs --check
10 */
11
12 import { resolveCredentials } from './lib/cookies.js';
13 import { TwitterClientBase } from './lib/twitter-client-base.js';
14 import { withSearch } from './lib/twitter-client-search.js';
15
16 // Build a search-only client (no posting, bookmarks, etc.)
17 const SearchClient = withSearch(TwitterClientBase);
18
19 const args = process.argv.slice(2);
20
21 function writeStdout(text) {
22 if (text) process.stdout.write(text);
23 }
24
25 function writeStderr(text) {
26 if (text) process.stderr.write(text);
27 }
28
29 async function main() {
30 // --check: verify that credentials can be resolved
31 if (args.includes('--check')) {
32 try {
33 const { cookies, warnings } = await resolveCredentials({});
34 if (cookies.authToken && cookies.ct0) {
35 writeStdout(JSON.stringify({ authenticated: true, source: cookies.source }));
36 return 0;
37 }
38 writeStdout(JSON.stringify({ authenticated: false, warnings }));
39 return 1;
40 } catch (err) {
41 writeStdout(JSON.stringify({ authenticated: false, error: err.message }));
42 return 1;
43 }
44 }
45
46 // --whoami: check auth and output source
47 if (args.includes('--whoami')) {
48 try {
49 const { cookies } = await resolveCredentials({});
50 if (cookies.authToken && cookies.ct0) {
51 writeStdout(cookies.source || 'authenticated');
52 return 0;
53 }
54 writeStderr('Not authenticated\n');
55 return 1;
56 } catch (err) {
57 writeStderr(`Auth check failed: ${err.message}\n`);
58 return 1;
59 }
60 }
61
62 // Parse search args
63 let query = null;
64 let count = 20;
65 let jsonOutput = false;
66
67 for (let i = 0; i < args.length; i++) {
68 if (args[i] === '--count' && args[i + 1]) {
69 count = parseInt(args[i + 1], 10);
70 i++;
71 } else if (args[i] === '-n' && args[i + 1]) {
72 count = parseInt(args[i + 1], 10);
73 i++;
74 } else if (args[i] === '--json') {
75 jsonOutput = true;
76 } else if (!args[i].startsWith('-')) {
77 query = args[i];
78 }
79 }
80
81 if (!query) {
82 writeStderr('Usage: node bird-search.mjs <query> [--count N] [--json]\n');
83 return 1;
84 }
85
86 try {
87 // Resolve credentials (env vars, then browser cookies)
88 const { cookies, warnings } = await resolveCredentials({});
89
90 if (!cookies.authToken || !cookies.ct0) {
91 const msg = warnings.length > 0 ? warnings.join('; ') : 'No Twitter credentials found';
92 if (jsonOutput) {
93 writeStdout(JSON.stringify({ error: msg, items: [] }));
94 } else {
95 writeStderr(`Error: ${msg}\n`);
96 }
97 return 1;
98 }
99
100 const client = new SearchClient({
101 cookies: {
102 authToken: cookies.authToken,
103 ct0: cookies.ct0,
104 cookieHeader: cookies.cookieHeader,
105 },
106 timeoutMs: 30000,
107 });
108
109 const result = await client.search(query, count);
110
111 if (!result.success) {
112 if (jsonOutput) {
113 writeStdout(JSON.stringify({ error: result.error, items: [] }));
114 } else {
115 writeStderr(`Search failed: ${result.error}\n`);
116 }
117 return 1;
118 }
119
120 const tweets = result.tweets || [];
121 if (jsonOutput) {
122 writeStdout(JSON.stringify(tweets));
123 } else {
124 for (const tweet of tweets) {
125 const author = tweet.author?.username || 'unknown';
126 writeStdout(`@${author}: ${tweet.text?.slice(0, 200)}\n\n`);
127 }
128 }
129
130 return 0;
131 } catch (err) {
132 if (jsonOutput) {
133 writeStdout(JSON.stringify({ error: err.message, items: [] }));
134 } else {
135 writeStderr(`Error: ${err.message}\n`);
136 }
137 return 1;
138 }
139 }
140
141 try {
142 const code = await main();
143 process.exitCode = Number.isInteger(code) ? code : 1;
144 } catch (err) {
145 writeStderr(`Fatal error: ${err?.message || err}\n`);
146 process.exitCode = 1;
147 }
148
148 lines Plain Text