felo-ai 0.2.7 → 0.2.10
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 +39 -30
- package/CONTRIBUTING.md +346 -346
- package/README.en.md +129 -129
- package/README.md +469 -414
- 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 -78
- package/felo-web-fetch/SKILL.md +204 -200
- package/felo-web-fetch/scripts/run_web_fetch.mjs +316 -232
- package/felo-x-search/README.md +81 -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/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
|
@@ -1,232 +1,316 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const DEFAULT_API_BASE = 'https://openapi.felo.ai';
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'',
|
|
33
|
-
'
|
|
34
|
-
'
|
|
35
|
-
'
|
|
36
|
-
' --
|
|
37
|
-
' --
|
|
38
|
-
' --
|
|
39
|
-
' --
|
|
40
|
-
' --
|
|
41
|
-
' --json
|
|
42
|
-
' --
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (!
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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,81 @@
|
|
|
1
|
+
# Felo X Search Skill
|
|
2
|
+
|
|
3
|
+
Search X (Twitter) tweets, users, and replies using the [Felo X Search API](https://openapi.felo.ai/docs/api-reference/v2/x-search.html).
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Search tweets** by keyword with advanced query support
|
|
8
|
+
- **Search users** by keyword
|
|
9
|
+
- **Get user info** by username (batch supported)
|
|
10
|
+
- **Get user tweets** by username, with optional replies
|
|
11
|
+
- **Get tweet replies** by tweet ID
|
|
12
|
+
- Same `FELO_API_KEY` as other Felo skills
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
### 1) Configure API key
|
|
17
|
+
|
|
18
|
+
At [felo.ai](https://felo.ai) -> Settings -> API Keys, create a key, then:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Linux/macOS
|
|
22
|
+
export FELO_API_KEY="your-api-key-here"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```powershell
|
|
26
|
+
# Windows PowerShell
|
|
27
|
+
$env:FELO_API_KEY="your-api-key-here"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### 2) Run
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# From repo: script
|
|
34
|
+
node felo-x-search/scripts/run_x_search.mjs "AI news"
|
|
35
|
+
|
|
36
|
+
# After npm install -g felo-ai: CLI
|
|
37
|
+
felo x "AI news"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## All parameters
|
|
41
|
+
|
|
42
|
+
| Parameter | Option | Example |
|
|
43
|
+
|-----------|--------|---------|
|
|
44
|
+
| Search keyword | `[query]` or `-q, --query` | `felo x "AI news"` or `-q "AI news"` |
|
|
45
|
+
| Tweet IDs or usernames | `--id` | `--id "elonmusk"` or `--id "123,456"` |
|
|
46
|
+
| User mode | `--user` | `--user` |
|
|
47
|
+
| Get user tweets | `--tweets` | `--tweets` (with `--id --user`) |
|
|
48
|
+
| Result limit | `-l, --limit` | `--limit 20` |
|
|
49
|
+
| Pagination cursor | `--cursor` | `--cursor "abc123"` |
|
|
50
|
+
| Include replies | `--include-replies` | `--include-replies` (with `--tweets`) |
|
|
51
|
+
| Query type filter | `--query-type` | `--query-type "Latest"` |
|
|
52
|
+
| Start time filter | `--since-time` | `--since-time "2026-01-01"` |
|
|
53
|
+
| End time filter | `--until-time` | `--until-time "2026-03-01"` |
|
|
54
|
+
| Full JSON response | `-j, --json` | `-j` |
|
|
55
|
+
| Timeout (seconds) | `-t, --timeout` | `-t 60` |
|
|
56
|
+
|
|
57
|
+
## Usage patterns
|
|
58
|
+
|
|
59
|
+
**Search mode** (with query):
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
felo x "AI news" # Search tweets (default)
|
|
63
|
+
felo x "OpenAI" --user # Search users
|
|
64
|
+
felo x "AI" --limit 10 --json # Search with limit, raw JSON output
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Lookup mode** (with --id):
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
felo x --id "elonmusk" --user # Get user info
|
|
71
|
+
felo x --id "elonmusk,OpenAI" --user # Batch user info
|
|
72
|
+
felo x --id "elonmusk" --user --tweets # Get user tweets
|
|
73
|
+
felo x --id "elonmusk" --user --tweets --limit 20 # With limit
|
|
74
|
+
felo x --id "1234567890" # Get tweet replies
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## When to use (Agent)
|
|
78
|
+
|
|
79
|
+
Trigger keywords: twitter, tweet, X user, X search, tweets from, replies to, trending on X, 推特, 推文, `/felo-x-search`.
|
|
80
|
+
|
|
81
|
+
See [SKILL.md](SKILL.md) for full agent instructions and API parameters.
|