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.
Files changed (39) hide show
  1. package/.github/workflows/publish-npm.yml +39 -0
  2. package/CHANGELOG.md +30 -0
  3. package/CONTRIBUTING.md +346 -346
  4. package/README.en.md +129 -129
  5. package/README.md +435 -408
  6. package/docs/EXAMPLES.md +632 -632
  7. package/docs/FAQ.md +479 -479
  8. package/felo-search/LICENSE +21 -21
  9. package/felo-search/README.md +440 -440
  10. package/felo-search/SKILL.md +291 -291
  11. package/felo-slides/LICENSE +21 -21
  12. package/felo-slides/README.md +87 -87
  13. package/felo-slides/SKILL.md +166 -166
  14. package/felo-slides/scripts/run_ppt_task.mjs +251 -251
  15. package/felo-superAgent/LICENSE +21 -0
  16. package/felo-superAgent/README.md +125 -0
  17. package/felo-superAgent/SKILL.md +165 -0
  18. package/felo-web-fetch/README.md +127 -0
  19. package/felo-web-fetch/SKILL.md +204 -0
  20. package/felo-web-fetch/scripts/run_web_fetch.mjs +316 -0
  21. package/felo-x-search/SKILL.md +204 -0
  22. package/felo-x-search/scripts/run_x_search.mjs +385 -0
  23. package/felo-youtube-subtitling/README.md +59 -59
  24. package/felo-youtube-subtitling/SKILL.md +161 -161
  25. package/felo-youtube-subtitling/scripts/run_youtube_subtitling.mjs +239 -239
  26. package/package.json +37 -35
  27. package/src/cli.js +370 -252
  28. package/src/config.js +66 -66
  29. package/src/search.js +142 -142
  30. package/src/slides.js +332 -332
  31. package/src/superAgent.js +609 -0
  32. package/src/{webExtract.js → webFetch.js} +148 -148
  33. package/src/xSearch.js +366 -0
  34. package/src/youtubeSubtitling.js +179 -179
  35. package/tests/config.test.js +78 -78
  36. package/tests/search.test.js +100 -100
  37. package/felo-web-extract/README.md +0 -78
  38. package/felo-web-extract/SKILL.md +0 -200
  39. 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/)