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,385 @@
1
+ #!/usr/bin/env node
2
+
3
+ const DEFAULT_API_BASE = 'https://openapi.felo.ai';
4
+ const DEFAULT_TIMEOUT_MS = 30_000;
5
+ const MAX_RETRIES = 3;
6
+ const RETRY_BASE_MS = 1000;
7
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
8
+ const SPINNER_INTERVAL_MS = 80;
9
+ const STATUS_PAD = 56;
10
+
11
+ function startSpinner(message) {
12
+ const start = Date.now();
13
+ let i = 0;
14
+ const id = setInterval(() => {
15
+ const elapsed = Math.floor((Date.now() - start) / 1000);
16
+ const line = `${message} ${SPINNER_FRAMES[i % SPINNER_FRAMES.length]} ${elapsed}s`;
17
+ process.stderr.write(`\r${line.padEnd(STATUS_PAD, ' ')}`);
18
+ i += 1;
19
+ }, SPINNER_INTERVAL_MS);
20
+ return id;
21
+ }
22
+
23
+ function stopSpinner(id) {
24
+ if (id != null) clearInterval(id);
25
+ process.stderr.write(`\r${' '.repeat(STATUS_PAD)}\r`);
26
+ }
27
+
28
+ function sleep(ms) {
29
+ return new Promise((resolve) => setTimeout(resolve, ms));
30
+ }
31
+
32
+ function getMessage(payload) {
33
+ return payload?.message || payload?.error || payload?.msg || payload?.code || 'Unknown error';
34
+ }
35
+
36
+ async function fetchWithRetry(url, init, timeoutMs) {
37
+ let lastError;
38
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
39
+ const controller = new AbortController();
40
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
41
+ try {
42
+ const res = await fetch(url, { ...init, signal: controller.signal });
43
+ if (res.status >= 500 && attempt < MAX_RETRIES) {
44
+ await sleep(RETRY_BASE_MS * Math.pow(2, attempt));
45
+ continue;
46
+ }
47
+ return res;
48
+ } catch (err) {
49
+ lastError = err;
50
+ if (err.name === 'AbortError') throw new Error(`Request timed out after ${timeoutMs / 1000}s`);
51
+ if (attempt < MAX_RETRIES) {
52
+ await sleep(RETRY_BASE_MS * Math.pow(2, attempt));
53
+ continue;
54
+ }
55
+ throw lastError;
56
+ } finally {
57
+ clearTimeout(timer);
58
+ }
59
+ }
60
+ throw lastError;
61
+ }
62
+
63
+ async function postApi(apiBase, apiKey, path, body, timeoutMs) {
64
+ const res = await fetchWithRetry(
65
+ `${apiBase}${path}`,
66
+ {
67
+ method: 'POST',
68
+ headers: {
69
+ Accept: 'application/json',
70
+ Authorization: `Bearer ${apiKey}`,
71
+ 'Content-Type': 'application/json',
72
+ },
73
+ body: JSON.stringify(body),
74
+ },
75
+ timeoutMs,
76
+ );
77
+ let data = {};
78
+ try { data = await res.json(); } catch { data = {}; }
79
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${getMessage(data)}`);
80
+ if (data.status === 'error') throw new Error(getMessage(data));
81
+ return data;
82
+ }
83
+
84
+ // ── Formatting ──
85
+
86
+ function formatNumber(num) {
87
+ if (num == null) return '0';
88
+ if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(1)}B`;
89
+ if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(1)}M`;
90
+ if (num >= 1_000) return `${(num / 1_000).toFixed(1)}K`;
91
+ return String(num);
92
+ }
93
+
94
+ function formatUser(u, headerLevel = 2) {
95
+ if (!u) return '';
96
+ const h = '#'.repeat(Math.min(6, headerLevel));
97
+ let badge = '';
98
+ if (u.blue_verified) badge = ' 🔵';
99
+ else if (u.verified) badge = ' ✓';
100
+
101
+ let out = `${h} @${u.username} (${u.display_name || u.username}${badge})\n`;
102
+ out += `- User ID: \`${u.user_id}\`\n`;
103
+ if (u.verified_type) out += `- Verification Type: ${u.verified_type}\n`;
104
+ const status = [];
105
+ if (u.protected) status.push('Protected (Private)');
106
+ if (u.is_automated) status.push('Automated');
107
+ if (status.length) out += `- Account Status: ${status.join(' | ')}\n`;
108
+ out += `- Followers: ${formatNumber(u.followers_count)}\n`;
109
+ out += `- Following: ${formatNumber(u.following_count)}\n`;
110
+ out += `- Total Tweets: ${formatNumber(u.tweet_count)}\n`;
111
+ if (u.favorites_count > 0) out += `- Likes Given: ${formatNumber(u.favorites_count)}\n`;
112
+ if (u.media_count > 0) out += `- Media Count: ${formatNumber(u.media_count)}\n`;
113
+ if (u.description) out += `- Bio: ${u.description}\n`;
114
+ if (u.location) out += `- Location: ${u.location}\n`;
115
+ if (u.url) out += `- Website: ${u.url}\n`;
116
+ if (u.can_dm) out += `- Direct Messages: Open\n`;
117
+ if (u.profile_image_url) out += `- Profile Image: ${u.profile_image_url}\n`;
118
+ if (u.cover_image_url) out += `- Cover Image: ${u.cover_image_url}\n`;
119
+ if (u.pinned_tweet_ids?.length) out += `- Pinned Tweets: ${u.pinned_tweet_ids.length} tweet(s)\n`;
120
+ if (u.created_at) out += `- Account Created: ${u.created_at}\n`;
121
+ out += '\n---\n\n';
122
+ return out;
123
+ }
124
+
125
+ function formatTweet(t, indent = '', headerLevel = 3) {
126
+ if (!t) return '';
127
+ const h = '#'.repeat(Math.min(6, headerLevel));
128
+ const author = t.author || {};
129
+ const verified = author.verified ? ' ✓' : '';
130
+
131
+ let out = `${indent}${h} @${author.username || 'unknown'}(${author.display_name || author.username || 'unknown'}${verified})\n`;
132
+ const meta = [`Posted: ${t.created_at || 'unknown'}`, `Tweet ID: \`${t.id}\``];
133
+ if (t.conversation_id) meta.push(`Conversation: \`${t.conversation_id}\``);
134
+ if (t.is_reply && t.in_reply_to_username) meta.push(`Reply to @${t.in_reply_to_username}`);
135
+ out += `${indent}- ${meta.join(' | ')}\n\n`;
136
+
137
+ for (const line of (t.content || '').split('\n')) {
138
+ out += `${indent}${line}\n`;
139
+ }
140
+ out += '\n';
141
+
142
+ const metrics = t.metrics || {};
143
+ const parts = [];
144
+ if (metrics.favorite_count) parts.push(`${formatNumber(metrics.favorite_count)} likes`);
145
+ if (metrics.retweet_count) parts.push(`${formatNumber(metrics.retweet_count)} retweets`);
146
+ if (metrics.reply_count) parts.push(`${formatNumber(metrics.reply_count)} replies`);
147
+ if (parts.length) out += `${indent}Engagement: ${parts.join(' | ')}\n`;
148
+
149
+ if (Array.isArray(t.media_urls) && t.media_urls.length) {
150
+ out += `${indent}Media (${t.media_urls.length}):\n`;
151
+ for (const media of t.media_urls) {
152
+ const type = media.type || 'photo';
153
+ if (type === 'video') {
154
+ out += `${indent} • [video] ${media.url || ''}\n`;
155
+ } else {
156
+ out += `${indent} • [photo] ${media.thumbnail || ''}\n`;
157
+ }
158
+ }
159
+ }
160
+
161
+ out += `\n${indent}---\n\n`;
162
+ return out;
163
+ }
164
+
165
+ // ── CLI ──
166
+
167
+ function usage() {
168
+ console.error(
169
+ [
170
+ 'Usage:',
171
+ ' node felo-x-search/scripts/run_x_search.mjs [query] [options]',
172
+ '',
173
+ 'Examples:',
174
+ ' run_x_search.mjs "AI news" Search tweets (default)',
175
+ ' run_x_search.mjs "OpenAI" --user Search users',
176
+ ' run_x_search.mjs --id "1234567890" Get tweet replies',
177
+ ' run_x_search.mjs --id "elonmusk" --user Get user info',
178
+ ' run_x_search.mjs --id "elonmusk" --user --tweets Get user tweets',
179
+ '',
180
+ 'Options:',
181
+ ' -q, --query <text> Search keyword (same as positional arg)',
182
+ ' --id <values> Tweet IDs or usernames (comma-separated)',
183
+ ' --user Switch to user mode',
184
+ ' --tweets Get user tweets (with --id --user)',
185
+ ' -l, --limit <n> Number of results',
186
+ ' --cursor <str> Pagination cursor',
187
+ ' --include-replies Include replies (with --tweets)',
188
+ ' --query-type <type> Query type filter',
189
+ ' --since-time <val> Start time filter',
190
+ ' --until-time <val> End time filter',
191
+ ' -j, --json Output raw JSON',
192
+ ' -t, --timeout <ms> Request timeout in ms (default: 30000)',
193
+ ' --help Show this help',
194
+ ].join('\n'),
195
+ );
196
+ }
197
+
198
+ function parseArgs(argv) {
199
+ const out = {
200
+ query: '',
201
+ ids: [],
202
+ user: false,
203
+ tweets: false,
204
+ limit: 0,
205
+ cursor: '',
206
+ includeReplies: false,
207
+ queryType: '',
208
+ sinceTime: null,
209
+ untilTime: null,
210
+ json: false,
211
+ timeoutMs: DEFAULT_TIMEOUT_MS,
212
+ help: false,
213
+ };
214
+
215
+ const positional = [];
216
+
217
+ for (let i = 0; i < argv.length; i++) {
218
+ const a = argv[i];
219
+ if (a === '--help' || a === '-h') { out.help = true; }
220
+ else if (a === '--json' || a === '-j') { out.json = true; }
221
+ else if (a === '--user') { out.user = true; }
222
+ else if (a === '--tweets') { out.tweets = true; }
223
+ else if (a === '--include-replies') { out.includeReplies = true; }
224
+ else if (a === '-q' || a === '--query') { out.query = (argv[++i] || '').trim(); }
225
+ else if (a === '--id') { out.ids = (argv[++i] || '').split(',').map((s) => s.trim()).filter(Boolean); }
226
+ else if (a === '-l' || a === '--limit') { out.limit = parseInt(argv[++i] || '', 10) || 0; }
227
+ else if (a === '--cursor') { out.cursor = (argv[++i] || '').trim(); }
228
+ else if (a === '--query-type') { out.queryType = (argv[++i] || '').trim(); }
229
+ else if (a === '--since-time') { out.sinceTime = argv[++i]; }
230
+ else if (a === '--until-time') { out.untilTime = argv[++i]; }
231
+ else if (a === '-t' || a === '--timeout') {
232
+ const n = parseInt(argv[++i] || '', 10);
233
+ if (Number.isFinite(n) && n > 0) out.timeoutMs = n;
234
+ }
235
+ else if (!a.startsWith('-')) { positional.push(a); }
236
+ }
237
+
238
+ // positional arg as query
239
+ if (!out.query && positional.length) {
240
+ out.query = positional.join(' ').trim();
241
+ }
242
+
243
+ return out;
244
+ }
245
+
246
+ async function main() {
247
+ const args = parseArgs(process.argv.slice(2));
248
+ if (args.help) { usage(); process.exit(0); }
249
+
250
+ const apiKey = process.env.FELO_API_KEY?.trim();
251
+ if (!apiKey) {
252
+ console.error('ERROR: FELO_API_KEY not set');
253
+ process.exit(1);
254
+ }
255
+
256
+ const apiBase = (process.env.FELO_API_BASE?.trim() || DEFAULT_API_BASE).replace(/\/$/, '');
257
+ const { query, ids, json, timeoutMs } = args;
258
+
259
+ if (!query && !ids.length) {
260
+ usage();
261
+ process.exit(1);
262
+ }
263
+
264
+ let code = 1;
265
+ try {
266
+ if (query) {
267
+ if (args.user) {
268
+ // search users
269
+ const spinnerId = startSpinner(`Searching users: ${query}`);
270
+ try {
271
+ const body = { query };
272
+ if (args.cursor) body.cursor = args.cursor;
273
+ const payload = await postApi(apiBase, apiKey, '/x/user/search', body, timeoutMs);
274
+ if (json) { console.log(JSON.stringify(payload, null, 2)); code = 0; }
275
+ else {
276
+ const data = payload?.data || {};
277
+ const users = data.users || [];
278
+ if (!users.length) { process.stderr.write('No users found.\n'); }
279
+ else {
280
+ process.stdout.write(`Found ${data.total || users.length} user(s)\n\n`);
281
+ for (const u of users) process.stdout.write(formatUser(u));
282
+ if (data.has_next && data.next_cursor) process.stderr.write(`\nMore results available. Use --cursor "${data.next_cursor}"\n`);
283
+ code = 0;
284
+ }
285
+ }
286
+ } finally { stopSpinner(spinnerId); }
287
+ } else {
288
+ // search tweets (default)
289
+ const spinnerId = startSpinner(`Searching tweets: ${query}`);
290
+ try {
291
+ const body = { query };
292
+ if (args.queryType) body.query_type = args.queryType;
293
+ if (args.sinceTime) body.since_time = args.sinceTime;
294
+ if (args.untilTime) body.until_time = args.untilTime;
295
+ if (args.limit) body.limit = args.limit;
296
+ if (args.cursor) body.cursor = args.cursor;
297
+ const payload = await postApi(apiBase, apiKey, '/x/tweet/search', body, timeoutMs);
298
+ if (json) { console.log(JSON.stringify(payload, null, 2)); code = 0; }
299
+ else {
300
+ const data = payload?.data || {};
301
+ const tweets = data.tweets || [];
302
+ if (!tweets.length) { process.stderr.write('No tweets found.\n'); }
303
+ else {
304
+ process.stdout.write(`Found ${data.total || tweets.length} tweet(s)\n\n`);
305
+ for (const t of tweets) process.stdout.write(formatTweet(t));
306
+ if (data.has_next && data.next_cursor) process.stderr.write(`\nMore results available. Use --cursor "${data.next_cursor}"\n`);
307
+ code = 0;
308
+ }
309
+ }
310
+ } finally { stopSpinner(spinnerId); }
311
+ }
312
+ } else if (ids.length) {
313
+ if (args.user) {
314
+ if (args.tweets) {
315
+ // get user tweets
316
+ const label = ids[0];
317
+ const spinnerId = startSpinner(`Fetching tweets from ${label}`);
318
+ try {
319
+ const body = { username: ids[0] };
320
+ if (args.limit) body.limit = args.limit;
321
+ if (args.cursor) body.cursor = args.cursor;
322
+ if (args.includeReplies) body.include_replies = true;
323
+ const payload = await postApi(apiBase, apiKey, '/x/user/tweets', body, timeoutMs);
324
+ if (json) { console.log(JSON.stringify(payload, null, 2)); code = 0; }
325
+ else {
326
+ const data = payload?.data || {};
327
+ const tweets = data.tweets || [];
328
+ if (!tweets.length) { process.stderr.write('No tweets found.\n'); }
329
+ else {
330
+ process.stdout.write(`Found ${data.total || tweets.length} tweet(s)\n\n`);
331
+ for (const t of tweets) process.stdout.write(formatTweet(t));
332
+ if (data.has_next && data.next_cursor) process.stderr.write(`\nMore results available. Use --cursor "${data.next_cursor}"\n`);
333
+ code = 0;
334
+ }
335
+ }
336
+ } finally { stopSpinner(spinnerId); }
337
+ } else {
338
+ // get user info
339
+ const spinnerId = startSpinner('Fetching user info');
340
+ try {
341
+ const payload = await postApi(apiBase, apiKey, '/x/user/info', { usernames: ids }, timeoutMs);
342
+ if (json) { console.log(JSON.stringify(payload, null, 2)); code = 0; }
343
+ else {
344
+ const users = payload?.data?.users || [];
345
+ if (!users.length) { process.stderr.write('No users found.\n'); }
346
+ else { for (const u of users) process.stdout.write(formatUser(u)); code = 0; }
347
+ }
348
+ } finally { stopSpinner(spinnerId); }
349
+ }
350
+ } else {
351
+ // get tweet replies (default for --id)
352
+ const spinnerId = startSpinner(`Fetching replies for ${ids.length} tweet(s)`);
353
+ try {
354
+ const body = { tweet_ids: ids };
355
+ if (args.cursor) body.cursor = args.cursor;
356
+ if (args.sinceTime) body.since_time = args.sinceTime;
357
+ if (args.untilTime) body.until_time = args.untilTime;
358
+ const payload = await postApi(apiBase, apiKey, '/x/tweet/replies', body, timeoutMs);
359
+ if (json) { console.log(JSON.stringify(payload, null, 2)); code = 0; }
360
+ else {
361
+ const results = payload?.data?.results || [];
362
+ if (!results.length) { process.stderr.write('No replies found.\n'); }
363
+ else {
364
+ for (const r of results) {
365
+ process.stdout.write(`## Replies to tweet \`${r.tweet_id}\` (${r.total || 0} total)\n\n`);
366
+ for (const t of (r.replies || [])) process.stdout.write(formatTweet(t));
367
+ if (r.has_next && r.next_cursor) process.stderr.write(`More replies for ${r.tweet_id}. Use --cursor "${r.next_cursor}"\n`);
368
+ }
369
+ code = 0;
370
+ }
371
+ }
372
+ } finally { stopSpinner(spinnerId); }
373
+ }
374
+ }
375
+ } catch (err) {
376
+ process.stderr.write(`Error: ${err?.message || err}\n`);
377
+ }
378
+
379
+ process.exit(code);
380
+ }
381
+
382
+ main().catch((err) => {
383
+ process.stderr.write(`Fatal: ${err?.message || err}\n`);
384
+ process.exit(1);
385
+ });
@@ -1,59 +1,59 @@
1
- # Felo YouTube Subtitling Skill
2
-
3
- Fetch YouTube video subtitles/captions using the [Felo YouTube Subtitling API](https://openapi.felo.ai/docs/api-reference/v2/youtube-subtitling.html).
4
-
5
- ## Features
6
-
7
- - Get subtitles by **YouTube video URL** or **video ID** (e.g. `https://youtube.com/watch?v=ID` or `dQw4w9WgXcQ`)
8
- - Optional **language** (e.g. `en`, `zh-CN`)
9
- - Optional **timestamps** (`--with-time`) for each segment
10
- - Same `FELO_API_KEY` as other Felo skills
11
-
12
- ## Quick Start
13
-
14
- ### 1) Configure API key
15
-
16
- At [felo.ai](https://felo.ai) -> Settings -> API Keys, create a key, then:
17
-
18
- ```bash
19
- # Linux/macOS
20
- export FELO_API_KEY="your-api-key-here"
21
- ```
22
-
23
- ```powershell
24
- # Windows PowerShell
25
- $env:FELO_API_KEY="your-api-key-here"
26
- ```
27
-
28
- ### 2) Run
29
-
30
- ```bash
31
- # From repo: script (URL or video ID)
32
- node felo-youtube-subtitling/scripts/run_youtube_subtitling.mjs --video-code "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
33
-
34
- # After npm install -g felo-ai: CLI
35
- felo youtube-subtitling -v "https://youtu.be/dQw4w9WgXcQ"
36
- felo youtube-subtitling -v "dQw4w9WgXcQ"
37
- ```
38
-
39
- ## All parameters
40
-
41
- | Parameter | Option | Example |
42
- |-----------|--------|---------|
43
- | Video URL or ID (required) | `-v`, `--video-code` | `--video-code "https://youtube.com/watch?v=ID"` or `"dQw4w9WgXcQ"` |
44
- | Language | `-l`, `--language` | `--language zh-CN` |
45
- | Include timestamps | `--with-time` | `--with-time` |
46
- | Full JSON response | `-j`, `--json` | `-j` |
47
-
48
- **Examples**
49
-
50
- ```bash
51
- felo youtube-subtitling -v "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
52
- felo youtube-subtitling -v "dQw4w9WgXcQ" --language en --with-time -j
53
- ```
54
-
55
- ## When to use (Agent)
56
-
57
- Trigger keywords: YouTube subtitles, get captions, video transcript, extract subtitles, `/felo-youtube-subtitling`.
58
-
59
- See [SKILL.md](SKILL.md) for full agent instructions and API parameters.
1
+ # Felo YouTube Subtitling Skill
2
+
3
+ Fetch YouTube video subtitles/captions using the [Felo YouTube Subtitling API](https://openapi.felo.ai/docs/api-reference/v2/youtube-subtitling.html).
4
+
5
+ ## Features
6
+
7
+ - Get subtitles by **YouTube video URL** or **video ID** (e.g. `https://youtube.com/watch?v=ID` or `dQw4w9WgXcQ`)
8
+ - Optional **language** (e.g. `en`, `zh-CN`)
9
+ - Optional **timestamps** (`--with-time`) for each segment
10
+ - Same `FELO_API_KEY` as other Felo skills
11
+
12
+ ## Quick Start
13
+
14
+ ### 1) Configure API key
15
+
16
+ At [felo.ai](https://felo.ai) -> Settings -> API Keys, create a key, then:
17
+
18
+ ```bash
19
+ # Linux/macOS
20
+ export FELO_API_KEY="your-api-key-here"
21
+ ```
22
+
23
+ ```powershell
24
+ # Windows PowerShell
25
+ $env:FELO_API_KEY="your-api-key-here"
26
+ ```
27
+
28
+ ### 2) Run
29
+
30
+ ```bash
31
+ # From repo: script (URL or video ID)
32
+ node felo-youtube-subtitling/scripts/run_youtube_subtitling.mjs --video-code "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
33
+
34
+ # After npm install -g felo-ai: CLI
35
+ felo youtube-subtitling -v "https://youtu.be/dQw4w9WgXcQ"
36
+ felo youtube-subtitling -v "dQw4w9WgXcQ"
37
+ ```
38
+
39
+ ## All parameters
40
+
41
+ | Parameter | Option | Example |
42
+ |-----------|--------|---------|
43
+ | Video URL or ID (required) | `-v`, `--video-code` | `--video-code "https://youtube.com/watch?v=ID"` or `"dQw4w9WgXcQ"` |
44
+ | Language | `-l`, `--language` | `--language zh-CN` |
45
+ | Include timestamps | `--with-time` | `--with-time` |
46
+ | Full JSON response | `-j`, `--json` | `-j` |
47
+
48
+ **Examples**
49
+
50
+ ```bash
51
+ felo youtube-subtitling -v "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
52
+ felo youtube-subtitling -v "dQw4w9WgXcQ" --language en --with-time -j
53
+ ```
54
+
55
+ ## When to use (Agent)
56
+
57
+ Trigger keywords: YouTube subtitles, get captions, video transcript, extract subtitles, `/felo-youtube-subtitling`.
58
+
59
+ See [SKILL.md](SKILL.md) for full agent instructions and API parameters.