felo-ai 0.2.6 → 0.2.9
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/.github/workflows/publish-npm.yml +39 -0
- package/CHANGELOG.md +30 -0
- package/CONTRIBUTING.md +346 -346
- package/README.en.md +129 -129
- package/README.md +435 -408
- package/docs/EXAMPLES.md +632 -632
- package/docs/FAQ.md +479 -479
- package/felo-search/LICENSE +21 -21
- package/felo-search/README.md +440 -440
- package/felo-search/SKILL.md +291 -291
- package/felo-slides/LICENSE +21 -21
- package/felo-slides/README.md +87 -87
- package/felo-slides/SKILL.md +166 -166
- package/felo-slides/scripts/run_ppt_task.mjs +251 -251
- package/felo-superAgent/LICENSE +21 -0
- package/felo-superAgent/README.md +125 -0
- package/felo-superAgent/SKILL.md +165 -0
- package/felo-web-fetch/README.md +127 -0
- package/felo-web-fetch/SKILL.md +204 -0
- package/felo-web-fetch/scripts/run_web_fetch.mjs +316 -0
- package/felo-x-search/SKILL.md +204 -0
- package/felo-x-search/scripts/run_x_search.mjs +385 -0
- package/felo-youtube-subtitling/README.md +59 -59
- package/felo-youtube-subtitling/SKILL.md +161 -161
- package/felo-youtube-subtitling/scripts/run_youtube_subtitling.mjs +239 -239
- package/package.json +37 -35
- package/src/cli.js +370 -252
- package/src/config.js +66 -66
- package/src/search.js +142 -142
- package/src/slides.js +332 -332
- package/src/superAgent.js +609 -0
- package/src/{webExtract.js → webFetch.js} +148 -148
- package/src/xSearch.js +366 -0
- package/src/youtubeSubtitling.js +179 -179
- package/tests/config.test.js +78 -78
- package/tests/search.test.js +100 -100
- package/felo-web-extract/README.md +0 -78
- package/felo-web-extract/SKILL.md +0 -200
- package/felo-web-extract/scripts/run_web_extract.mjs +0 -232
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const DEFAULT_API_BASE = 'https://openapi.felo.ai';
|
|
4
|
+
const DEFAULT_TIMEOUT_SEC = 60;
|
|
5
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
6
|
+
const SPINNER_INTERVAL_MS = 80;
|
|
7
|
+
const STATUS_PAD = 56;
|
|
8
|
+
|
|
9
|
+
function startSpinner(message) {
|
|
10
|
+
const start = Date.now();
|
|
11
|
+
let i = 0;
|
|
12
|
+
const id = setInterval(() => {
|
|
13
|
+
const elapsed = Math.floor((Date.now() - start) / 1000);
|
|
14
|
+
const line = `${message} ${SPINNER_FRAMES[i % SPINNER_FRAMES.length]} ${elapsed}s`;
|
|
15
|
+
process.stderr.write(`\r${line.padEnd(STATUS_PAD, ' ')}`);
|
|
16
|
+
i += 1;
|
|
17
|
+
}, SPINNER_INTERVAL_MS);
|
|
18
|
+
return id;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function stopSpinner(id) {
|
|
22
|
+
if (id != null) clearInterval(id);
|
|
23
|
+
process.stderr.write(`\r${' '.repeat(STATUS_PAD)}\r`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function usage() {
|
|
27
|
+
console.error(
|
|
28
|
+
[
|
|
29
|
+
'Usage:',
|
|
30
|
+
' node felo-web-fetch/scripts/run_web_fetch.mjs --url <url> [options]',
|
|
31
|
+
'',
|
|
32
|
+
'Required:',
|
|
33
|
+
' --url <url> Target page URL',
|
|
34
|
+
'',
|
|
35
|
+
'Options:',
|
|
36
|
+
' --output-format <format> html | markdown | text',
|
|
37
|
+
' --crawl-mode <mode> fast | fine',
|
|
38
|
+
' --target-selector <selector> CSS selector for target extraction',
|
|
39
|
+
' --wait-for-selector <selector> Wait until selector appears',
|
|
40
|
+
' --cookie <cookie> Add cookie entry (repeatable)',
|
|
41
|
+
' --set-cookies-json <json> JSON array for set_cookies',
|
|
42
|
+
' --user-agent <ua> Custom user-agent',
|
|
43
|
+
' --timeout <seconds> Request timeout in seconds (default 60)',
|
|
44
|
+
' --request-timeout-ms <ms> API timeout parameter in milliseconds',
|
|
45
|
+
' --with-readability <bool> true | false',
|
|
46
|
+
' --with-links-summary <bool> true | false',
|
|
47
|
+
' --with-images-summary <bool> true | false',
|
|
48
|
+
' --with-images-readability <bool> true | false',
|
|
49
|
+
' --with-images <bool> true | false',
|
|
50
|
+
' --with-links <bool> true | false',
|
|
51
|
+
' --ignore-empty-text-image <bool> true | false',
|
|
52
|
+
' --with-cache <bool> true | false',
|
|
53
|
+
' --with-stypes <bool> true | false',
|
|
54
|
+
' --json Print full JSON response',
|
|
55
|
+
' --help Show this help',
|
|
56
|
+
].join('\n')
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseBool(v, name) {
|
|
61
|
+
if (typeof v !== 'string') {
|
|
62
|
+
throw new Error(`Missing value for ${name}`);
|
|
63
|
+
}
|
|
64
|
+
const normalized = v.trim().toLowerCase();
|
|
65
|
+
if (normalized === 'true') return true;
|
|
66
|
+
if (normalized === 'false') return false;
|
|
67
|
+
throw new Error(`Invalid boolean for ${name}: ${v}. Use true or false.`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parseArgs(argv) {
|
|
71
|
+
const out = {
|
|
72
|
+
url: '',
|
|
73
|
+
outputFormat: '',
|
|
74
|
+
crawlMode: '',
|
|
75
|
+
targetSelector: '',
|
|
76
|
+
waitForSelector: '',
|
|
77
|
+
cookies: [],
|
|
78
|
+
cookiesJson: '',
|
|
79
|
+
userAgent: '',
|
|
80
|
+
timeoutSec: DEFAULT_TIMEOUT_SEC,
|
|
81
|
+
requestTimeoutMs: null,
|
|
82
|
+
withReadability: null,
|
|
83
|
+
withLinksSummary: null,
|
|
84
|
+
withImagesSummary: null,
|
|
85
|
+
withImagesReadability: null,
|
|
86
|
+
withImages: null,
|
|
87
|
+
withLinks: null,
|
|
88
|
+
ignoreEmptyTextImage: null,
|
|
89
|
+
withCache: null,
|
|
90
|
+
withStypes: null,
|
|
91
|
+
json: false,
|
|
92
|
+
help: false,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
96
|
+
const a = argv[i];
|
|
97
|
+
if (a === '--help' || a === '-h') {
|
|
98
|
+
out.help = true;
|
|
99
|
+
} else if (a === '--json') {
|
|
100
|
+
out.json = true;
|
|
101
|
+
} else if (a === '--url') {
|
|
102
|
+
out.url = argv[i + 1] ?? '';
|
|
103
|
+
i += 1;
|
|
104
|
+
} else if (a === '--output-format') {
|
|
105
|
+
out.outputFormat = (argv[i + 1] ?? '').trim().toLowerCase();
|
|
106
|
+
i += 1;
|
|
107
|
+
} else if (a === '--crawl-mode') {
|
|
108
|
+
out.crawlMode = (argv[i + 1] ?? '').trim().toLowerCase();
|
|
109
|
+
i += 1;
|
|
110
|
+
} else if (a === '--target-selector') {
|
|
111
|
+
out.targetSelector = argv[i + 1] ?? '';
|
|
112
|
+
i += 1;
|
|
113
|
+
} else if (a === '--wait-for-selector') {
|
|
114
|
+
out.waitForSelector = argv[i + 1] ?? '';
|
|
115
|
+
i += 1;
|
|
116
|
+
} else if (a === '--cookie') {
|
|
117
|
+
const value = argv[i + 1] ?? '';
|
|
118
|
+
if (value) out.cookies.push(value);
|
|
119
|
+
i += 1;
|
|
120
|
+
} else if (a === '--set-cookies-json') {
|
|
121
|
+
out.cookiesJson = argv[i + 1] ?? '';
|
|
122
|
+
i += 1;
|
|
123
|
+
} else if (a === '--user-agent') {
|
|
124
|
+
out.userAgent = argv[i + 1] ?? '';
|
|
125
|
+
i += 1;
|
|
126
|
+
} else if (a === '--timeout') {
|
|
127
|
+
out.timeoutSec = Number.parseInt(argv[i + 1] ?? '', 10);
|
|
128
|
+
i += 1;
|
|
129
|
+
} else if (a === '--request-timeout-ms') {
|
|
130
|
+
out.requestTimeoutMs = Number.parseInt(argv[i + 1] ?? '', 10);
|
|
131
|
+
i += 1;
|
|
132
|
+
} else if (a === '--with-readability') {
|
|
133
|
+
out.withReadability = parseBool(argv[i + 1], '--with-readability');
|
|
134
|
+
i += 1;
|
|
135
|
+
} else if (a === '--with-links-summary') {
|
|
136
|
+
out.withLinksSummary = parseBool(argv[i + 1], '--with-links-summary');
|
|
137
|
+
i += 1;
|
|
138
|
+
} else if (a === '--with-images-summary') {
|
|
139
|
+
out.withImagesSummary = parseBool(argv[i + 1], '--with-images-summary');
|
|
140
|
+
i += 1;
|
|
141
|
+
} else if (a === '--with-images-readability') {
|
|
142
|
+
out.withImagesReadability = parseBool(argv[i + 1], '--with-images-readability');
|
|
143
|
+
i += 1;
|
|
144
|
+
} else if (a === '--with-images') {
|
|
145
|
+
out.withImages = parseBool(argv[i + 1], '--with-images');
|
|
146
|
+
i += 1;
|
|
147
|
+
} else if (a === '--with-links') {
|
|
148
|
+
out.withLinks = parseBool(argv[i + 1], '--with-links');
|
|
149
|
+
i += 1;
|
|
150
|
+
} else if (a === '--ignore-empty-text-image') {
|
|
151
|
+
out.ignoreEmptyTextImage = parseBool(argv[i + 1], '--ignore-empty-text-image');
|
|
152
|
+
i += 1;
|
|
153
|
+
} else if (a === '--with-cache') {
|
|
154
|
+
out.withCache = parseBool(argv[i + 1], '--with-cache');
|
|
155
|
+
i += 1;
|
|
156
|
+
} else if (a === '--with-stypes') {
|
|
157
|
+
out.withStypes = parseBool(argv[i + 1], '--with-stypes');
|
|
158
|
+
i += 1;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!Number.isFinite(out.timeoutSec) || out.timeoutSec <= 0) {
|
|
163
|
+
out.timeoutSec = DEFAULT_TIMEOUT_SEC;
|
|
164
|
+
}
|
|
165
|
+
if (out.requestTimeoutMs !== null && (!Number.isFinite(out.requestTimeoutMs) || out.requestTimeoutMs <= 0)) {
|
|
166
|
+
out.requestTimeoutMs = null;
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function ensureInSet(value, allowed, fieldName) {
|
|
172
|
+
if (!value) return;
|
|
173
|
+
if (!allowed.includes(value)) {
|
|
174
|
+
throw new Error(`Invalid ${fieldName}: ${value}. Allowed values: ${allowed.join(', ')}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function isApiError(payload) {
|
|
179
|
+
if (typeof payload?.code === 'number') {
|
|
180
|
+
return payload.code !== 0;
|
|
181
|
+
}
|
|
182
|
+
if (typeof payload?.status === 'string') {
|
|
183
|
+
return payload.status.toLowerCase() === 'error';
|
|
184
|
+
}
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function getMessage(payload) {
|
|
189
|
+
return String(payload?.message || payload?.error || payload?.msg || 'Unknown error');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function fetchJson(url, init, timeoutMs) {
|
|
193
|
+
const controller = new AbortController();
|
|
194
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
195
|
+
try {
|
|
196
|
+
const res = await fetch(url, { ...init, signal: controller.signal });
|
|
197
|
+
let body = {};
|
|
198
|
+
try {
|
|
199
|
+
body = await res.json();
|
|
200
|
+
} catch {
|
|
201
|
+
body = {};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!res.ok) {
|
|
205
|
+
throw new Error(`HTTP ${res.status}: ${getMessage(body)}`);
|
|
206
|
+
}
|
|
207
|
+
if (isApiError(body)) {
|
|
208
|
+
throw new Error(getMessage(body));
|
|
209
|
+
}
|
|
210
|
+
return body;
|
|
211
|
+
} finally {
|
|
212
|
+
clearTimeout(timer);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function buildPayload(args) {
|
|
217
|
+
const payload = {
|
|
218
|
+
url: args.url,
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
if (args.outputFormat) payload.output_format = args.outputFormat;
|
|
222
|
+
if (args.crawlMode) payload.crawl_mode = args.crawlMode;
|
|
223
|
+
if (args.targetSelector) payload.target_selector = args.targetSelector;
|
|
224
|
+
if (args.waitForSelector) payload.wait_for_selector = args.waitForSelector;
|
|
225
|
+
if (args.userAgent) payload.user_agent = args.userAgent;
|
|
226
|
+
if (args.requestTimeoutMs !== null) payload.timeout = args.requestTimeoutMs;
|
|
227
|
+
|
|
228
|
+
if (args.cookies.length) payload.set_cookies = args.cookies;
|
|
229
|
+
if (args.cookiesJson) {
|
|
230
|
+
try {
|
|
231
|
+
const parsed = JSON.parse(args.cookiesJson);
|
|
232
|
+
if (!Array.isArray(parsed)) {
|
|
233
|
+
throw new Error('set_cookies JSON must be an array');
|
|
234
|
+
}
|
|
235
|
+
payload.set_cookies = parsed;
|
|
236
|
+
} catch (err) {
|
|
237
|
+
throw new Error(`Invalid --set-cookies-json: ${String(err.message || err)}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (args.withReadability !== null) payload.with_readability = args.withReadability;
|
|
242
|
+
if (args.withLinksSummary !== null) payload.with_links_summary = args.withLinksSummary;
|
|
243
|
+
if (args.withImagesSummary !== null) payload.with_images_summary = args.withImagesSummary;
|
|
244
|
+
if (args.withImagesReadability !== null) payload.with_images_readability = args.withImagesReadability;
|
|
245
|
+
if (args.withImages !== null) payload.with_images = args.withImages;
|
|
246
|
+
if (args.withLinks !== null) payload.with_links = args.withLinks;
|
|
247
|
+
if (args.ignoreEmptyTextImage !== null) payload.ignore_empty_text_image = args.ignoreEmptyTextImage;
|
|
248
|
+
if (args.withCache !== null) payload.with_cache = args.withCache;
|
|
249
|
+
if (args.withStypes !== null) payload.with_stypes = args.withStypes;
|
|
250
|
+
|
|
251
|
+
return payload;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function main() {
|
|
255
|
+
const args = parseArgs(process.argv.slice(2));
|
|
256
|
+
if (args.help) {
|
|
257
|
+
usage();
|
|
258
|
+
process.exit(0);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!args.url) {
|
|
262
|
+
usage();
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
ensureInSet(args.outputFormat, ['html', 'markdown', 'text'], 'output-format');
|
|
267
|
+
ensureInSet(args.crawlMode, ['fast', 'fine'], 'crawl-mode');
|
|
268
|
+
|
|
269
|
+
const apiKey = process.env.FELO_API_KEY?.trim();
|
|
270
|
+
if (!apiKey) {
|
|
271
|
+
console.error('ERROR: FELO_API_KEY not set');
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const apiBase = (process.env.FELO_API_BASE?.trim() || DEFAULT_API_BASE).replace(/\/$/, '');
|
|
276
|
+
const payload = buildPayload(args);
|
|
277
|
+
|
|
278
|
+
const shortUrl = args.url.length > 45 ? args.url.slice(0, 42) + '...' : args.url;
|
|
279
|
+
const spinnerId = startSpinner(`Fetching ${shortUrl}`);
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
const response = await fetchJson(
|
|
283
|
+
`${apiBase}/v2/web/extract`,
|
|
284
|
+
{
|
|
285
|
+
method: 'POST',
|
|
286
|
+
headers: {
|
|
287
|
+
Accept: 'application/json',
|
|
288
|
+
Authorization: `Bearer ${apiKey}`,
|
|
289
|
+
'Content-Type': 'application/json',
|
|
290
|
+
},
|
|
291
|
+
body: JSON.stringify(payload),
|
|
292
|
+
},
|
|
293
|
+
args.timeoutSec * 1000
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
if (args.json) {
|
|
297
|
+
console.log(JSON.stringify(response, null, 2));
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const content = response?.data?.content;
|
|
302
|
+
if (typeof content === 'string') {
|
|
303
|
+
console.log(content);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
console.log(JSON.stringify(content ?? response?.data ?? response, null, 2));
|
|
308
|
+
} catch (err) {
|
|
309
|
+
console.error(`ERROR: ${String(err?.message || err || 'Unknown error')}`);
|
|
310
|
+
process.exit(1);
|
|
311
|
+
} finally {
|
|
312
|
+
stopSpinner(spinnerId);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
main();
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: felo-x-search
|
|
3
|
+
description: "Search X (Twitter) data using Felo X Search API. Use when users ask about X/Twitter users, tweets, trending topics on X, tweet replies, or when explicit commands like /felo-x-search are used. Supports user lookup, user search, user tweets, tweet search, and tweet replies."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Felo X Search Skill
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
Trigger this skill when the user wants to:
|
|
11
|
+
|
|
12
|
+
- Look up X (Twitter) user profiles by username
|
|
13
|
+
- Search for X users by keyword
|
|
14
|
+
- Get tweets from a specific X user
|
|
15
|
+
- Search tweets by keyword or advanced query
|
|
16
|
+
- Get replies to specific tweets
|
|
17
|
+
|
|
18
|
+
Trigger keywords (examples):
|
|
19
|
+
|
|
20
|
+
- English: twitter, tweet, X user, X search, tweets from, replies to, trending on X
|
|
21
|
+
- 简体中文: 推特, 推文, X用户, X搜索, 推文回复
|
|
22
|
+
- 日本語: ツイッター, ツイート, Xユーザー, X検索
|
|
23
|
+
|
|
24
|
+
Explicit commands: `/felo-x-search`, "search X", "search twitter"
|
|
25
|
+
|
|
26
|
+
Do NOT use for:
|
|
27
|
+
|
|
28
|
+
- General web search (use `felo-search`)
|
|
29
|
+
- Webpage extraction (use `felo-web-extract`)
|
|
30
|
+
- Generating slides (use `felo-slides`)
|
|
31
|
+
|
|
32
|
+
## Setup
|
|
33
|
+
|
|
34
|
+
### 1. Get API key
|
|
35
|
+
|
|
36
|
+
1. Visit [felo.ai](https://felo.ai)
|
|
37
|
+
2. Open Settings -> API Keys
|
|
38
|
+
3. Create and copy your API key
|
|
39
|
+
|
|
40
|
+
### 2. Configure environment variable
|
|
41
|
+
|
|
42
|
+
Linux/macOS:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
export FELO_API_KEY="your-api-key-here"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Windows PowerShell:
|
|
49
|
+
|
|
50
|
+
```powershell
|
|
51
|
+
$env:FELO_API_KEY="your-api-key-here"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## How to Execute
|
|
55
|
+
|
|
56
|
+
### Option A: Use the bundled script or packaged CLI
|
|
57
|
+
|
|
58
|
+
**Packaged CLI** (after `npm install -g felo-ai`):
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
felo x [query] [options]
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Script** (from repo):
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
node felo-x-search/scripts/run_x_search.mjs [query] [options]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Usage Pattern
|
|
71
|
+
|
|
72
|
+
The `x` command uses parameter combinations to infer intent — no subcommands needed.
|
|
73
|
+
|
|
74
|
+
**With query (search mode):**
|
|
75
|
+
|
|
76
|
+
| Usage | Behavior |
|
|
77
|
+
|-------|----------|
|
|
78
|
+
| `felo x "AI news"` | Search tweets (default) |
|
|
79
|
+
| `felo x -q "AI news"` | Same as above |
|
|
80
|
+
| `felo x "OpenAI" --user` | Search users |
|
|
81
|
+
|
|
82
|
+
**With --id (lookup mode):**
|
|
83
|
+
|
|
84
|
+
| Usage | Behavior |
|
|
85
|
+
|-------|----------|
|
|
86
|
+
| `felo x --id "1234567890"` | Get tweet replies (default) |
|
|
87
|
+
| `felo x --id "elonmusk" --user` | Get user info |
|
|
88
|
+
| `felo x --id "elonmusk" --user --tweets` | Get user tweets |
|
|
89
|
+
|
|
90
|
+
### Options
|
|
91
|
+
|
|
92
|
+
| Option | Description |
|
|
93
|
+
|--------|-------------|
|
|
94
|
+
| `[query]` | Positional arg, search keyword (equivalent to -q) |
|
|
95
|
+
| `-q, --query <text>` | Search keyword |
|
|
96
|
+
| `--id <values>` | Tweet IDs or usernames (comma-separated) |
|
|
97
|
+
| `--user` | Switch to user mode |
|
|
98
|
+
| `--tweets` | Get user tweets (with --id --user) |
|
|
99
|
+
| `-l, --limit <n>` | Number of results |
|
|
100
|
+
| `--cursor <str>` | Pagination cursor |
|
|
101
|
+
| `--include-replies` | Include replies (with --tweets) |
|
|
102
|
+
| `--query-type <type>` | Query type filter (tweet search) |
|
|
103
|
+
| `--since-time <val>` | Start time filter |
|
|
104
|
+
| `--until-time <val>` | End time filter |
|
|
105
|
+
| `-j, --json` | Output raw JSON |
|
|
106
|
+
| `-t, --timeout <seconds>` | Timeout in seconds (default: 30) |
|
|
107
|
+
|
|
108
|
+
### Option B: Call API with curl
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# Search tweets
|
|
112
|
+
curl -X POST "https://openapi.felo.ai/x/tweet/search" \
|
|
113
|
+
-H "Authorization: Bearer $FELO_API_KEY" \
|
|
114
|
+
-H "Content-Type: application/json" \
|
|
115
|
+
-d '{"query": "AI news", "limit": 20}'
|
|
116
|
+
|
|
117
|
+
# Search users
|
|
118
|
+
curl -X POST "https://openapi.felo.ai/x/user/search" \
|
|
119
|
+
-H "Authorization: Bearer $FELO_API_KEY" \
|
|
120
|
+
-H "Content-Type: application/json" \
|
|
121
|
+
-d '{"query": "artificial intelligence"}'
|
|
122
|
+
|
|
123
|
+
# Get user info
|
|
124
|
+
curl -X POST "https://openapi.felo.ai/x/user/info" \
|
|
125
|
+
-H "Authorization: Bearer $FELO_API_KEY" \
|
|
126
|
+
-H "Content-Type: application/json" \
|
|
127
|
+
-d '{"usernames": ["elonmusk", "OpenAI"]}'
|
|
128
|
+
|
|
129
|
+
# Get user tweets
|
|
130
|
+
curl -X POST "https://openapi.felo.ai/x/user/tweets" \
|
|
131
|
+
-H "Authorization: Bearer $FELO_API_KEY" \
|
|
132
|
+
-H "Content-Type: application/json" \
|
|
133
|
+
-d '{"username": "elonmusk", "limit": 20}'
|
|
134
|
+
|
|
135
|
+
# Get tweet replies
|
|
136
|
+
curl -X POST "https://openapi.felo.ai/x/tweet/replies" \
|
|
137
|
+
-H "Authorization: Bearer $FELO_API_KEY" \
|
|
138
|
+
-H "Content-Type: application/json" \
|
|
139
|
+
-d '{"tweet_ids": ["1234567890"]}'
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Output Format
|
|
143
|
+
|
|
144
|
+
### User Info (default, non-JSON)
|
|
145
|
+
|
|
146
|
+
```markdown
|
|
147
|
+
## @elonmusk (Elon Musk 🔵)
|
|
148
|
+
- User ID: `44196397`
|
|
149
|
+
- Followers: 100.0M
|
|
150
|
+
- Following: 500
|
|
151
|
+
- Total Tweets: 30.0K
|
|
152
|
+
- Bio: ...
|
|
153
|
+
- Location: Mars
|
|
154
|
+
- Account Created: 2009-06-02T00:00:00Z
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Tweet (default, non-JSON)
|
|
160
|
+
|
|
161
|
+
```markdown
|
|
162
|
+
### @elonmusk(Elon Musk ✓)
|
|
163
|
+
- Posted: 2026-03-09T12:00:00Z | Tweet ID: `1234567890`
|
|
164
|
+
|
|
165
|
+
Tweet content here...
|
|
166
|
+
|
|
167
|
+
Engagement: 5.0K likes | 1.0K retweets | 200 replies
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Error Handling
|
|
173
|
+
|
|
174
|
+
### Common Error Codes
|
|
175
|
+
|
|
176
|
+
- `INVALID_API_KEY` — API Key is invalid or revoked
|
|
177
|
+
- `X_SEARCH_FAILED` — X search request failed (check parameters or downstream error)
|
|
178
|
+
|
|
179
|
+
### Missing API Key
|
|
180
|
+
|
|
181
|
+
If `FELO_API_KEY` is not set, display setup instructions and stop.
|
|
182
|
+
|
|
183
|
+
## API Reference (summary)
|
|
184
|
+
|
|
185
|
+
- **Base URL**: `https://openapi.felo.ai`. Override with `FELO_API_BASE` env if needed.
|
|
186
|
+
- **Auth**: `Authorization: Bearer YOUR_API_KEY`
|
|
187
|
+
- **Endpoints**:
|
|
188
|
+
- `POST /x/user/info` — Batch get user profiles
|
|
189
|
+
- `POST /x/user/search` — Search users
|
|
190
|
+
- `POST /x/user/tweets` — Get user tweets
|
|
191
|
+
- `POST /x/tweet/search` — Search tweets
|
|
192
|
+
- `POST /x/tweet/replies` — Get tweet replies
|
|
193
|
+
|
|
194
|
+
## Important Notes
|
|
195
|
+
|
|
196
|
+
- Always check `FELO_API_KEY` before calling; if missing, return setup instructions.
|
|
197
|
+
- Format output as readable Markdown by default; use `--json` for raw API response.
|
|
198
|
+
- Use pagination cursors (`next_cursor`) for fetching more results.
|
|
199
|
+
- For tweet search, advanced query syntax is supported (same as X advanced search).
|
|
200
|
+
|
|
201
|
+
## References
|
|
202
|
+
|
|
203
|
+
- [Felo X Search API](https://openapi.felo.ai/docs/api-reference/v2/x-search.html)
|
|
204
|
+
- [Felo Open Platform](https://openapi.felo.ai/docs/)
|