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
@@ -1,239 +1,239 @@
1
- #!/usr/bin/env node
2
-
3
- const DEFAULT_API_BASE = 'https://openapi.felo.ai';
4
- const DEFAULT_TIMEOUT_MS = 30_000;
5
- const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
6
- const SPINNER_INTERVAL_MS = 80;
7
- const STATUS_PAD = 52;
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
- /** Extract video ID from YouTube URL or return string if plain ID. Returns null if invalid. */
27
- function extractVideoId(urlOrId) {
28
- const s = typeof urlOrId === 'string' ? urlOrId.trim() : '';
29
- if (!s) return null;
30
- try {
31
- if (s.startsWith('http://') || s.startsWith('https://')) {
32
- const u = new URL(s);
33
- if (u.hostname === 'youtu.be') return u.pathname.slice(1).split('?')[0] || null;
34
- if (u.hostname === 'www.youtube.com' || u.hostname === 'youtube.com') {
35
- if (u.pathname === '/watch') return u.searchParams.get('v');
36
- const m = u.pathname.match(/^\/(?:embed|v)\/([a-zA-Z0-9_-]{10,12})/);
37
- if (m) return m[1];
38
- return u.searchParams.get('v');
39
- }
40
- }
41
- if (/^[a-zA-Z0-9_-]{10,12}$/.test(s)) return s;
42
- return null;
43
- } catch {
44
- return null;
45
- }
46
- }
47
-
48
- function usage() {
49
- console.error(
50
- [
51
- 'Usage:',
52
- ' node felo-youtube-subtitling/scripts/run_youtube_subtitling.mjs --video-code <url-or-id> [options]',
53
- '',
54
- 'Options:',
55
- ' --video-code, -v <url-or-id> YouTube video URL or 11-char video ID (required)',
56
- ' --language, -l <code> Subtitle language (e.g. en, zh-CN)',
57
- ' --with-time Include start/duration per segment',
58
- ' --json, -j Print full API response as JSON',
59
- ' --help Show this help',
60
- ].join('\n')
61
- );
62
- }
63
-
64
- function parseArgs(argv) {
65
- const out = {
66
- videoCode: '',
67
- language: '',
68
- withTime: false,
69
- json: false,
70
- };
71
-
72
- for (let i = 0; i < argv.length; i += 1) {
73
- const a = argv[i];
74
- if (a === '--help' || a === '-h') {
75
- out.help = true;
76
- } else if (a === '--json' || a === '-j') {
77
- out.json = true;
78
- } else if (a === '--with-time') {
79
- out.withTime = true;
80
- } else if (a === '--video-code' || a === '-v') {
81
- const next = argv[i + 1];
82
- if (next === undefined || next === null || String(next).trim() === '' || String(next).startsWith('-')) {
83
- out.videoCode = '';
84
- } else {
85
- out.videoCode = String(next).trim();
86
- }
87
- i += 1;
88
- } else if (a === '--language' || a === '-l') {
89
- out.language = (argv[i + 1] ?? '').trim();
90
- i += 1;
91
- }
92
- }
93
-
94
- return out;
95
- }
96
-
97
- function getMessage(payload) {
98
- return (
99
- payload?.message ||
100
- payload?.error ||
101
- payload?.msg ||
102
- payload?.code ||
103
- 'Unknown error'
104
- );
105
- }
106
-
107
- async function fetchJson(url, init, timeoutMs) {
108
- const controller = new AbortController();
109
- const timer = setTimeout(() => controller.abort(), timeoutMs);
110
- try {
111
- const res = await fetch(url, { ...init, signal: controller.signal });
112
- let body = {};
113
- try {
114
- body = await res.json();
115
- } catch {
116
- body = {};
117
- }
118
-
119
- if (!res.ok) {
120
- throw new Error(`HTTP ${res.status}: ${getMessage(body)}`);
121
- }
122
- const code = body.code;
123
- const hasData = body?.data != null;
124
- const successCodes = [0, 200];
125
- const hasSuccessCode =
126
- successCodes.includes(Number(code)) ||
127
- code === undefined ||
128
- code === null ||
129
- (hasData && res.ok);
130
- if (!hasSuccessCode) {
131
- throw new Error(getMessage(body));
132
- }
133
- return body;
134
- } finally {
135
- clearTimeout(timer);
136
- }
137
- }
138
-
139
- function formatContents(contents, withTime) {
140
- if (!Array.isArray(contents) || contents.length === 0) return '';
141
- return contents
142
- .map((c) => {
143
- if (withTime && (c.start != null || c.duration != null)) {
144
- const start = Number(c.start);
145
- const dur = Number(c.duration);
146
- const startSec = Number.isFinite(start) ? start : 0;
147
- const durSec = Number.isFinite(dur) ? dur : 0;
148
- return `[${startSec.toFixed(2)}s, +${durSec.toFixed(2)}s] ${c.text ?? ''}`;
149
- }
150
- return c.text ?? '';
151
- })
152
- .filter(Boolean)
153
- .join('\n');
154
- }
155
-
156
- async function main() {
157
- const args = parseArgs(process.argv.slice(2));
158
- if (args.help) {
159
- usage();
160
- process.exit(0);
161
- }
162
- const videoCode = extractVideoId(args.videoCode);
163
- if (!videoCode) {
164
- if (!args.videoCode || String(args.videoCode).trim() === '') {
165
- usage();
166
- } else {
167
- console.error('ERROR: Invalid YouTube URL or video ID. Use a link (e.g. https://youtube.com/watch?v=ID) or an 11-character video ID.');
168
- }
169
- process.exit(1);
170
- }
171
-
172
- const apiKey = process.env.FELO_API_KEY?.trim();
173
- if (!apiKey) {
174
- console.error('ERROR: FELO_API_KEY not set');
175
- process.exit(1);
176
- }
177
-
178
- const apiBase = (process.env.FELO_API_BASE?.trim() || DEFAULT_API_BASE).replace(/\/$/, '');
179
-
180
- const spinnerId = startSpinner(`Fetching subtitles ${videoCode}`);
181
-
182
- try {
183
- const params = new URLSearchParams({ video_code: videoCode });
184
- if (args.language) params.set('language', args.language);
185
- if (args.withTime) params.set('with_time', 'true');
186
-
187
- const url = `${apiBase}/v2/youtube/subtitling?${params.toString()}`;
188
-
189
- const payload = await fetchJson(
190
- url,
191
- {
192
- method: 'GET',
193
- headers: {
194
- Accept: 'application/json',
195
- Authorization: `Bearer ${apiKey}`,
196
- },
197
- },
198
- DEFAULT_TIMEOUT_MS
199
- );
200
-
201
- const data = payload?.data ?? {};
202
- const title = data?.title ?? '';
203
- const contents = data?.contents ?? [];
204
-
205
- if (args.json) {
206
- console.log(JSON.stringify(payload, null, 2));
207
- return;
208
- }
209
-
210
- const text = formatContents(contents, args.withTime);
211
- const isEmpty = !text || text.trim() === '';
212
-
213
- if (isEmpty) {
214
- stopSpinner(spinnerId);
215
- process.stderr.write(
216
- `No subtitles found for video ${videoCode}. The video may have no captions or the language is not available.\n`
217
- );
218
- process.exit(1);
219
- }
220
-
221
- if (title) {
222
- console.log(`# ${title}\n`);
223
- }
224
- console.log(text);
225
- } finally {
226
- stopSpinner(spinnerId);
227
- }
228
- }
229
-
230
- main().catch((err) => {
231
- let videoCode = '';
232
- const argv = process.argv.slice(2);
233
- const i = argv.findIndex((a) => a === '--video-code' || a === '-v');
234
- if (i >= 0 && argv[i + 1]) videoCode = argv[i + 1];
235
- process.stderr.write(
236
- `YouTube subtitling failed${videoCode ? ` for video ${videoCode}` : ''}: ${err?.message || err}\n`
237
- );
238
- process.exit(1);
239
- });
1
+ #!/usr/bin/env node
2
+
3
+ const DEFAULT_API_BASE = 'https://openapi.felo.ai';
4
+ const DEFAULT_TIMEOUT_MS = 30_000;
5
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
6
+ const SPINNER_INTERVAL_MS = 80;
7
+ const STATUS_PAD = 52;
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
+ /** Extract video ID from YouTube URL or return string if plain ID. Returns null if invalid. */
27
+ function extractVideoId(urlOrId) {
28
+ const s = typeof urlOrId === 'string' ? urlOrId.trim() : '';
29
+ if (!s) return null;
30
+ try {
31
+ if (s.startsWith('http://') || s.startsWith('https://')) {
32
+ const u = new URL(s);
33
+ if (u.hostname === 'youtu.be') return u.pathname.slice(1).split('?')[0] || null;
34
+ if (u.hostname === 'www.youtube.com' || u.hostname === 'youtube.com') {
35
+ if (u.pathname === '/watch') return u.searchParams.get('v');
36
+ const m = u.pathname.match(/^\/(?:embed|v)\/([a-zA-Z0-9_-]{10,12})/);
37
+ if (m) return m[1];
38
+ return u.searchParams.get('v');
39
+ }
40
+ }
41
+ if (/^[a-zA-Z0-9_-]{10,12}$/.test(s)) return s;
42
+ return null;
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ function usage() {
49
+ console.error(
50
+ [
51
+ 'Usage:',
52
+ ' node felo-youtube-subtitling/scripts/run_youtube_subtitling.mjs --video-code <url-or-id> [options]',
53
+ '',
54
+ 'Options:',
55
+ ' --video-code, -v <url-or-id> YouTube video URL or 11-char video ID (required)',
56
+ ' --language, -l <code> Subtitle language (e.g. en, zh-CN)',
57
+ ' --with-time Include start/duration per segment',
58
+ ' --json, -j Print full API response as JSON',
59
+ ' --help Show this help',
60
+ ].join('\n')
61
+ );
62
+ }
63
+
64
+ function parseArgs(argv) {
65
+ const out = {
66
+ videoCode: '',
67
+ language: '',
68
+ withTime: false,
69
+ json: false,
70
+ };
71
+
72
+ for (let i = 0; i < argv.length; i += 1) {
73
+ const a = argv[i];
74
+ if (a === '--help' || a === '-h') {
75
+ out.help = true;
76
+ } else if (a === '--json' || a === '-j') {
77
+ out.json = true;
78
+ } else if (a === '--with-time') {
79
+ out.withTime = true;
80
+ } else if (a === '--video-code' || a === '-v') {
81
+ const next = argv[i + 1];
82
+ if (next === undefined || next === null || String(next).trim() === '' || String(next).startsWith('-')) {
83
+ out.videoCode = '';
84
+ } else {
85
+ out.videoCode = String(next).trim();
86
+ }
87
+ i += 1;
88
+ } else if (a === '--language' || a === '-l') {
89
+ out.language = (argv[i + 1] ?? '').trim();
90
+ i += 1;
91
+ }
92
+ }
93
+
94
+ return out;
95
+ }
96
+
97
+ function getMessage(payload) {
98
+ return (
99
+ payload?.message ||
100
+ payload?.error ||
101
+ payload?.msg ||
102
+ payload?.code ||
103
+ 'Unknown error'
104
+ );
105
+ }
106
+
107
+ async function fetchJson(url, init, timeoutMs) {
108
+ const controller = new AbortController();
109
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
110
+ try {
111
+ const res = await fetch(url, { ...init, signal: controller.signal });
112
+ let body = {};
113
+ try {
114
+ body = await res.json();
115
+ } catch {
116
+ body = {};
117
+ }
118
+
119
+ if (!res.ok) {
120
+ throw new Error(`HTTP ${res.status}: ${getMessage(body)}`);
121
+ }
122
+ const code = body.code;
123
+ const hasData = body?.data != null;
124
+ const successCodes = [0, 200];
125
+ const hasSuccessCode =
126
+ successCodes.includes(Number(code)) ||
127
+ code === undefined ||
128
+ code === null ||
129
+ (hasData && res.ok);
130
+ if (!hasSuccessCode) {
131
+ throw new Error(getMessage(body));
132
+ }
133
+ return body;
134
+ } finally {
135
+ clearTimeout(timer);
136
+ }
137
+ }
138
+
139
+ function formatContents(contents, withTime) {
140
+ if (!Array.isArray(contents) || contents.length === 0) return '';
141
+ return contents
142
+ .map((c) => {
143
+ if (withTime && (c.start != null || c.duration != null)) {
144
+ const start = Number(c.start);
145
+ const dur = Number(c.duration);
146
+ const startSec = Number.isFinite(start) ? start : 0;
147
+ const durSec = Number.isFinite(dur) ? dur : 0;
148
+ return `[${startSec.toFixed(2)}s, +${durSec.toFixed(2)}s] ${c.text ?? ''}`;
149
+ }
150
+ return c.text ?? '';
151
+ })
152
+ .filter(Boolean)
153
+ .join('\n');
154
+ }
155
+
156
+ async function main() {
157
+ const args = parseArgs(process.argv.slice(2));
158
+ if (args.help) {
159
+ usage();
160
+ process.exit(0);
161
+ }
162
+ const videoCode = extractVideoId(args.videoCode);
163
+ if (!videoCode) {
164
+ if (!args.videoCode || String(args.videoCode).trim() === '') {
165
+ usage();
166
+ } else {
167
+ console.error('ERROR: Invalid YouTube URL or video ID. Use a link (e.g. https://youtube.com/watch?v=ID) or an 11-character video ID.');
168
+ }
169
+ process.exit(1);
170
+ }
171
+
172
+ const apiKey = process.env.FELO_API_KEY?.trim();
173
+ if (!apiKey) {
174
+ console.error('ERROR: FELO_API_KEY not set');
175
+ process.exit(1);
176
+ }
177
+
178
+ const apiBase = (process.env.FELO_API_BASE?.trim() || DEFAULT_API_BASE).replace(/\/$/, '');
179
+
180
+ const spinnerId = startSpinner(`Fetching subtitles ${videoCode}`);
181
+
182
+ try {
183
+ const params = new URLSearchParams({ video_code: videoCode });
184
+ if (args.language) params.set('language', args.language);
185
+ if (args.withTime) params.set('with_time', 'true');
186
+
187
+ const url = `${apiBase}/v2/youtube/subtitling?${params.toString()}`;
188
+
189
+ const payload = await fetchJson(
190
+ url,
191
+ {
192
+ method: 'GET',
193
+ headers: {
194
+ Accept: 'application/json',
195
+ Authorization: `Bearer ${apiKey}`,
196
+ },
197
+ },
198
+ DEFAULT_TIMEOUT_MS
199
+ );
200
+
201
+ const data = payload?.data ?? {};
202
+ const title = data?.title ?? '';
203
+ const contents = data?.contents ?? [];
204
+
205
+ if (args.json) {
206
+ console.log(JSON.stringify(payload, null, 2));
207
+ return;
208
+ }
209
+
210
+ const text = formatContents(contents, args.withTime);
211
+ const isEmpty = !text || text.trim() === '';
212
+
213
+ if (isEmpty) {
214
+ stopSpinner(spinnerId);
215
+ process.stderr.write(
216
+ `No subtitles found for video ${videoCode}. The video may have no captions or the language is not available.\n`
217
+ );
218
+ process.exit(1);
219
+ }
220
+
221
+ if (title) {
222
+ console.log(`# ${title}\n`);
223
+ }
224
+ console.log(text);
225
+ } finally {
226
+ stopSpinner(spinnerId);
227
+ }
228
+ }
229
+
230
+ main().catch((err) => {
231
+ let videoCode = '';
232
+ const argv = process.argv.slice(2);
233
+ const i = argv.findIndex((a) => a === '--video-code' || a === '-v');
234
+ if (i >= 0 && argv[i + 1]) videoCode = argv[i + 1];
235
+ process.stderr.write(
236
+ `YouTube subtitling failed${videoCode ? ` for video ${videoCode}` : ''}: ${err?.message || err}\n`
237
+ );
238
+ process.exit(1);
239
+ });
package/package.json CHANGED
@@ -1,35 +1,37 @@
1
- {
2
- "name": "felo-ai",
3
- "version": "0.2.6",
4
- "description": "Felo AI CLI - real-time search, PPT generation, web extract, and YouTube subtitles from the terminal",
5
- "type": "module",
6
- "main": "src/cli.js",
7
- "bin": {
8
- "felo": "src/cli.js"
9
- },
10
- "engines": {
11
- "node": ">=18"
12
- },
13
- "keywords": [
14
- "felo",
15
- "felo-ai",
16
- "search",
17
- "slides",
18
- "web-extract",
19
- "youtube-subtitles",
20
- "cli",
21
- "ai"
22
- ],
23
- "license": "MIT",
24
- "repository": {
25
- "type": "git",
26
- "url": "https://github.com/Felo-Inc/felo-skills.git"
27
- },
28
- "dependencies": {
29
- "commander": "^12.0.0",
30
- "felo-search": "^0.1.1"
31
- },
32
- "scripts": {
33
- "test": "node --test \"tests/*.js\""
34
- }
35
- }
1
+ {
2
+ "name": "felo-ai",
3
+ "version": "0.2.9",
4
+ "description": "Felo AI CLI - real-time search, PPT generation, web fetch, and YouTube subtitles from the terminal",
5
+ "type": "module",
6
+ "main": "src/cli.js",
7
+ "bin": {
8
+ "felo": "src/cli.js"
9
+ },
10
+ "engines": {
11
+ "node": ">=18"
12
+ },
13
+ "keywords": [
14
+ "felo",
15
+ "felo-ai",
16
+ "search",
17
+ "slides",
18
+ "web-fetch",
19
+ "youtube-subtitles",
20
+ "x-search",
21
+ "twitter",
22
+ "cli",
23
+ "ai"
24
+ ],
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/Felo-Inc/felo-skills.git"
29
+ },
30
+ "dependencies": {
31
+ "commander": "^12.0.0",
32
+ "felo-search": "^0.1.1"
33
+ },
34
+ "scripts": {
35
+ "test": "node --test tests/"
36
+ }
37
+ }