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
package/src/config.js CHANGED
@@ -1,66 +1,66 @@
1
- import fs from 'fs/promises';
2
- import path from 'path';
3
- import os from 'os';
4
-
5
- function getConfigFilePath() {
6
- return process.env.FELO_CONFIG_FILE || path.join(os.homedir(), '.felo', 'config.json');
7
- }
8
-
9
- const SENSITIVE_KEYS = ['FELO_API_KEY', 'API_KEY', 'SECRET', 'TOKEN', 'PASSWORD'];
10
-
11
- async function ensureConfigDir() {
12
- await fs.mkdir(path.dirname(getConfigFilePath()), { recursive: true });
13
- }
14
-
15
- export async function getConfig() {
16
- const configFile = getConfigFilePath();
17
- try {
18
- const raw = await fs.readFile(configFile, 'utf-8');
19
- return JSON.parse(raw);
20
- } catch (e) {
21
- if (e.code === 'ENOENT') return {};
22
- if (e instanceof SyntaxError) {
23
- process.stderr.write('Warning: Invalid config file, using empty config.\n');
24
- return {};
25
- }
26
- throw e;
27
- }
28
- }
29
-
30
- export async function setConfig(key, value) {
31
- await ensureConfigDir();
32
- const config = await getConfig();
33
- config[key] = value;
34
- await fs.writeFile(getConfigFilePath(), JSON.stringify(config, null, 2), 'utf-8');
35
- }
36
-
37
- export async function getConfigValue(key) {
38
- const config = await getConfig();
39
- return config[key];
40
- }
41
-
42
- /** Returns value suitable for display; masks sensitive keys. */
43
- export function maskValueForDisplay(key, value) {
44
- if (value === undefined || value === null) return value;
45
- const s = String(value).trim();
46
- if (!s) return s;
47
- const upper = key.toUpperCase();
48
- const isSensitive = SENSITIVE_KEYS.some((k) => upper === k || upper.endsWith('_' + k));
49
- if (!isSensitive || s.length <= 8) return s;
50
- return s.slice(0, 4) + '...' + s.slice(-4);
51
- }
52
-
53
- export async function unsetConfig(key) {
54
- const config = await getConfig();
55
- delete config[key];
56
- await ensureConfigDir();
57
- await fs.writeFile(getConfigFilePath(), JSON.stringify(config, null, 2), 'utf-8');
58
- }
59
-
60
- export async function listConfig() {
61
- return getConfig();
62
- }
63
-
64
- export function getConfigPath() {
65
- return getConfigFilePath();
66
- }
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ function getConfigFilePath() {
6
+ return process.env.FELO_CONFIG_FILE || path.join(os.homedir(), '.felo', 'config.json');
7
+ }
8
+
9
+ const SENSITIVE_KEYS = ['FELO_API_KEY', 'API_KEY', 'SECRET', 'TOKEN', 'PASSWORD'];
10
+
11
+ async function ensureConfigDir() {
12
+ await fs.mkdir(path.dirname(getConfigFilePath()), { recursive: true });
13
+ }
14
+
15
+ export async function getConfig() {
16
+ const configFile = getConfigFilePath();
17
+ try {
18
+ const raw = await fs.readFile(configFile, 'utf-8');
19
+ return JSON.parse(raw);
20
+ } catch (e) {
21
+ if (e.code === 'ENOENT') return {};
22
+ if (e instanceof SyntaxError) {
23
+ process.stderr.write('Warning: Invalid config file, using empty config.\n');
24
+ return {};
25
+ }
26
+ throw e;
27
+ }
28
+ }
29
+
30
+ export async function setConfig(key, value) {
31
+ await ensureConfigDir();
32
+ const config = await getConfig();
33
+ config[key] = value;
34
+ await fs.writeFile(getConfigFilePath(), JSON.stringify(config, null, 2), 'utf-8');
35
+ }
36
+
37
+ export async function getConfigValue(key) {
38
+ const config = await getConfig();
39
+ return config[key];
40
+ }
41
+
42
+ /** Returns value suitable for display; masks sensitive keys. */
43
+ export function maskValueForDisplay(key, value) {
44
+ if (value === undefined || value === null) return value;
45
+ const s = String(value).trim();
46
+ if (!s) return s;
47
+ const upper = key.toUpperCase();
48
+ const isSensitive = SENSITIVE_KEYS.some((k) => upper === k || upper.endsWith('_' + k));
49
+ if (!isSensitive || s.length <= 8) return s;
50
+ return s.slice(0, 4) + '...' + s.slice(-4);
51
+ }
52
+
53
+ export async function unsetConfig(key) {
54
+ const config = await getConfig();
55
+ delete config[key];
56
+ await ensureConfigDir();
57
+ await fs.writeFile(getConfigFilePath(), JSON.stringify(config, null, 2), 'utf-8');
58
+ }
59
+
60
+ export async function listConfig() {
61
+ return getConfig();
62
+ }
63
+
64
+ export function getConfigPath() {
65
+ return getConfigFilePath();
66
+ }
package/src/search.js CHANGED
@@ -1,142 +1,142 @@
1
- const FELO_API = 'https://openapi.felo.ai/v2/chat';
2
- const DEFAULT_TIMEOUT_MS = 60_000;
3
- const MAX_RETRIES = 3;
4
- const RETRY_BASE_MS = 1000;
5
-
6
- const NO_KEY_MESSAGE = `
7
- ❌ Felo API Key not configured
8
-
9
- To use Felo CLI, set the FELO_API_KEY environment variable or run:
10
-
11
- felo config set FELO_API_KEY <your-api-key>
12
-
13
- Get your API key from https://felo.ai (Settings → API Keys).
14
- `;
15
-
16
- async function getApiKey() {
17
- if (process.env.FELO_API_KEY?.trim()) {
18
- return process.env.FELO_API_KEY.trim();
19
- }
20
- const { getConfigValue } = await import('./config.js');
21
- const fromConfig = await getConfigValue('FELO_API_KEY');
22
- return typeof fromConfig === 'string' ? fromConfig.trim() : '';
23
- }
24
-
25
- export { getApiKey, fetchWithTimeoutAndRetry, NO_KEY_MESSAGE };
26
-
27
- function sleep(ms) {
28
- return new Promise((resolve) => setTimeout(resolve, ms));
29
- }
30
-
31
- async function fetchWithTimeoutAndRetry(url, options, timeoutMs = DEFAULT_TIMEOUT_MS) {
32
- let lastError;
33
- for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
34
- const controller = new AbortController();
35
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
36
- try {
37
- const res = await fetch(url, {
38
- ...options,
39
- signal: controller.signal,
40
- });
41
- // Retry on 5xx (server errors)
42
- if (res.status >= 500 && attempt < MAX_RETRIES) {
43
- const delay = RETRY_BASE_MS * Math.pow(2, attempt);
44
- await sleep(delay);
45
- continue;
46
- }
47
- return res;
48
- } catch (err) {
49
- lastError = err;
50
- if (err.name === 'AbortError') {
51
- throw new Error(`Request timed out after ${timeoutMs / 1000}s`);
52
- }
53
- if (attempt < MAX_RETRIES) {
54
- const delay = RETRY_BASE_MS * Math.pow(2, attempt);
55
- await sleep(delay);
56
- continue;
57
- }
58
- throw lastError;
59
- } finally {
60
- clearTimeout(timeoutId);
61
- }
62
- }
63
- throw lastError;
64
- }
65
-
66
- export async function search(query, options = {}) {
67
- const apiKey = await getApiKey();
68
- if (!apiKey) {
69
- console.error(NO_KEY_MESSAGE.trim());
70
- return 1;
71
- }
72
-
73
- try {
74
- process.stderr.write('Searching...\n');
75
-
76
- const res = await fetchWithTimeoutAndRetry(
77
- FELO_API,
78
- {
79
- method: 'POST',
80
- headers: {
81
- 'Authorization': `Bearer ${apiKey}`,
82
- 'Content-Type': 'application/json',
83
- },
84
- body: JSON.stringify({ query: query.trim() }),
85
- },
86
- options.timeoutMs ?? DEFAULT_TIMEOUT_MS
87
- );
88
-
89
- const data = await res.json().catch(() => ({}));
90
-
91
- // API error response: { status: "error", code, message } (per doc)
92
- if (data.status === 'error') {
93
- const msg = data.message || data.code || 'Unknown error';
94
- console.error(`Error: ${msg}`);
95
- return 1;
96
- }
97
-
98
- if (!res.ok) {
99
- const msg = data.message || data.error || res.statusText || `HTTP ${res.status}`;
100
- console.error(`Error: ${msg}`);
101
- return 1;
102
- }
103
-
104
- // Success: { status: "ok", data: { answer, query_analysis: { queries }, resources } }
105
- const payload = data.data;
106
- if (!payload) {
107
- console.error('Error: Unexpected response format');
108
- return 1;
109
- }
110
-
111
- if (options.json) {
112
- console.log(JSON.stringify(data, null, 2));
113
- return 0;
114
- }
115
-
116
- // Default: only the answer (stdout, pipe-friendly)
117
- if (payload.answer) {
118
- console.log(payload.answer);
119
- }
120
-
121
- // Verbose: add query analysis and sources (per API doc)
122
- if (options.verbose) {
123
- const queries = payload.query_analysis?.queries;
124
- if (Array.isArray(queries) && queries.length) {
125
- process.stderr.write('\n## Query Analysis\n');
126
- process.stderr.write(`Optimized search terms: ${queries.join(', ')}\n`);
127
- }
128
- const resources = payload.resources;
129
- if (Array.isArray(resources) && resources.length) {
130
- process.stderr.write('\n## Sources\n');
131
- resources.forEach((r) => {
132
- process.stderr.write(`- ${r.title}: ${r.link}\n`);
133
- });
134
- }
135
- }
136
-
137
- return 0;
138
- } catch (err) {
139
- console.error('Error:', err.message || err);
140
- return 1;
141
- }
142
- }
1
+ const FELO_API = 'https://openapi.felo.ai/v2/chat';
2
+ const DEFAULT_TIMEOUT_MS = 60_000;
3
+ const MAX_RETRIES = 3;
4
+ const RETRY_BASE_MS = 1000;
5
+
6
+ const NO_KEY_MESSAGE = `
7
+ ❌ Felo API Key not configured
8
+
9
+ To use Felo CLI, set the FELO_API_KEY environment variable or run:
10
+
11
+ felo config set FELO_API_KEY <your-api-key>
12
+
13
+ Get your API key from https://felo.ai (Settings → API Keys).
14
+ `;
15
+
16
+ async function getApiKey() {
17
+ if (process.env.FELO_API_KEY?.trim()) {
18
+ return process.env.FELO_API_KEY.trim();
19
+ }
20
+ const { getConfigValue } = await import('./config.js');
21
+ const fromConfig = await getConfigValue('FELO_API_KEY');
22
+ return typeof fromConfig === 'string' ? fromConfig.trim() : '';
23
+ }
24
+
25
+ export { getApiKey, fetchWithTimeoutAndRetry, NO_KEY_MESSAGE };
26
+
27
+ function sleep(ms) {
28
+ return new Promise((resolve) => setTimeout(resolve, ms));
29
+ }
30
+
31
+ async function fetchWithTimeoutAndRetry(url, options, timeoutMs = DEFAULT_TIMEOUT_MS) {
32
+ let lastError;
33
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
34
+ const controller = new AbortController();
35
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
36
+ try {
37
+ const res = await fetch(url, {
38
+ ...options,
39
+ signal: controller.signal,
40
+ });
41
+ // Retry on 5xx (server errors)
42
+ if (res.status >= 500 && attempt < MAX_RETRIES) {
43
+ const delay = RETRY_BASE_MS * Math.pow(2, attempt);
44
+ await sleep(delay);
45
+ continue;
46
+ }
47
+ return res;
48
+ } catch (err) {
49
+ lastError = err;
50
+ if (err.name === 'AbortError') {
51
+ throw new Error(`Request timed out after ${timeoutMs / 1000}s`);
52
+ }
53
+ if (attempt < MAX_RETRIES) {
54
+ const delay = RETRY_BASE_MS * Math.pow(2, attempt);
55
+ await sleep(delay);
56
+ continue;
57
+ }
58
+ throw lastError;
59
+ } finally {
60
+ clearTimeout(timeoutId);
61
+ }
62
+ }
63
+ throw lastError;
64
+ }
65
+
66
+ export async function search(query, options = {}) {
67
+ const apiKey = await getApiKey();
68
+ if (!apiKey) {
69
+ console.error(NO_KEY_MESSAGE.trim());
70
+ return 1;
71
+ }
72
+
73
+ try {
74
+ process.stderr.write('Searching...\n');
75
+
76
+ const res = await fetchWithTimeoutAndRetry(
77
+ FELO_API,
78
+ {
79
+ method: 'POST',
80
+ headers: {
81
+ 'Authorization': `Bearer ${apiKey}`,
82
+ 'Content-Type': 'application/json',
83
+ },
84
+ body: JSON.stringify({ query: query.trim() }),
85
+ },
86
+ options.timeoutMs ?? DEFAULT_TIMEOUT_MS
87
+ );
88
+
89
+ const data = await res.json().catch(() => ({}));
90
+
91
+ // API error response: { status: "error", code, message } (per doc)
92
+ if (data.status === 'error') {
93
+ const msg = data.message || data.code || 'Unknown error';
94
+ console.error(`Error: ${msg}`);
95
+ return 1;
96
+ }
97
+
98
+ if (!res.ok) {
99
+ const msg = data.message || data.error || res.statusText || `HTTP ${res.status}`;
100
+ console.error(`Error: ${msg}`);
101
+ return 1;
102
+ }
103
+
104
+ // Success: { status: "ok", data: { answer, query_analysis: { queries }, resources } }
105
+ const payload = data.data;
106
+ if (!payload) {
107
+ console.error('Error: Unexpected response format');
108
+ return 1;
109
+ }
110
+
111
+ if (options.json) {
112
+ console.log(JSON.stringify(data, null, 2));
113
+ return 0;
114
+ }
115
+
116
+ // Default: only the answer (stdout, pipe-friendly)
117
+ if (payload.answer) {
118
+ console.log(payload.answer);
119
+ }
120
+
121
+ // Verbose: add query analysis and sources (per API doc)
122
+ if (options.verbose) {
123
+ const queries = payload.query_analysis?.queries;
124
+ if (Array.isArray(queries) && queries.length) {
125
+ process.stderr.write('\n## Query Analysis\n');
126
+ process.stderr.write(`Optimized search terms: ${queries.join(', ')}\n`);
127
+ }
128
+ const resources = payload.resources;
129
+ if (Array.isArray(resources) && resources.length) {
130
+ process.stderr.write('\n## Sources\n');
131
+ resources.forEach((r) => {
132
+ process.stderr.write(`- ${r.title}: ${r.link}\n`);
133
+ });
134
+ }
135
+ }
136
+
137
+ return 0;
138
+ } catch (err) {
139
+ console.error('Error:', err.message || err);
140
+ return 1;
141
+ }
142
+ }