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/cli.js CHANGED
@@ -1,252 +1,370 @@
1
- #!/usr/bin/env node
2
-
3
- import { createRequire } from "module";
4
- import { Command } from "commander";
5
- import { search } from "./search.js";
6
- import { slides } from "./slides.js";
7
- import { webExtract } from "./webExtract.js";
8
- import { youtubeSubtitling } from "./youtubeSubtitling.js";
9
- import * as config from "./config.js";
10
-
11
- const require = createRequire(import.meta.url);
12
- const pkg = require("../package.json");
13
-
14
- /** Delay (ms) before process.exit to let Windows libuv finish handle cleanup. */
15
- const EXIT_DELAY_MS = 50;
16
-
17
- /**
18
- * Flush stdout then stderr, then exit after a short delay. Avoids Node.js
19
- * Windows UV_HANDLE_CLOSING assertion when process.exit() runs while streams
20
- * or other handles are still closing.
21
- * @param {number} code - Exit code.
22
- */
23
- function flushStdioThenExit(code) {
24
- const doExit = () => setTimeout(() => process.exit(code), EXIT_DELAY_MS);
25
- const flushStderr = () => {
26
- if (process.stderr?.writable && !process.stderr.destroyed) {
27
- process.stderr.write("", () => doExit());
28
- } else {
29
- doExit();
30
- }
31
- };
32
- if (process.stdout?.writable && !process.stdout.destroyed) {
33
- process.stdout.write("", () => flushStderr());
34
- } else {
35
- flushStderr();
36
- }
37
- }
38
-
39
- const program = new Command();
40
-
41
- program
42
- .name("felo")
43
- .description("Felo AI CLI - real-time search from the terminal")
44
- .version(pkg.version);
45
-
46
- program
47
- .command("search")
48
- .description("Search for current information (weather, news, docs, etc.)")
49
- .argument("<query>", "search query")
50
- .option("-j, --json", "output raw JSON")
51
- .option("-v, --verbose", "show query analysis and sources")
52
- .option("-t, --timeout <seconds>", "request timeout in seconds", "60")
53
- .action(async (query, opts) => {
54
- const timeoutMs = parseInt(opts.timeout, 10) * 1000;
55
- const code = await search(query, {
56
- json: opts.json,
57
- verbose: opts.verbose,
58
- timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
59
- });
60
- process.exitCode = code;
61
- flushStdioThenExit(code);
62
- });
63
-
64
- program
65
- .command("slides")
66
- .description(
67
- "Generate PPT/slides from a prompt (async task, outputs live doc URL when done)"
68
- )
69
- .argument(
70
- "<query>",
71
- 'PPT generation prompt (e.g. "Felo, 2 pages" or "Introduction to React")'
72
- )
73
- .option("-j, --json", "output raw JSON with task_id and live_doc_url")
74
- .option("-v, --verbose", "show polling status")
75
- .option(
76
- "-t, --timeout <seconds>",
77
- "request timeout in seconds for each API call",
78
- "60"
79
- )
80
- .option(
81
- "--poll-timeout <seconds>",
82
- "max seconds to wait for task completion",
83
- "1200"
84
- )
85
- .action(async (query, opts) => {
86
- const timeoutMs = parseInt(opts.timeout, 10) * 1000;
87
- const pollTimeoutMs = parseInt(opts.pollTimeout, 10) * 1000 || 1_200_000;
88
- const code = await slides(query, {
89
- json: opts.json,
90
- verbose: opts.verbose,
91
- timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
92
- pollTimeoutMs: Number.isNaN(pollTimeoutMs) ? 1_200_000 : pollTimeoutMs,
93
- });
94
- process.exitCode = code;
95
- flushStdioThenExit(code);
96
- });
97
-
98
- const configCmd = program
99
- .command("config")
100
- .description(
101
- "Manage persisted config (e.g. FELO_API_KEY). Stored in ~/.felo/config.json"
102
- );
103
-
104
- configCmd
105
- .command("set <key> <value>")
106
- .description(
107
- "Set a config value (e.g. felo config set FELO_API_KEY your-key)"
108
- )
109
- .action(async (key, value) => {
110
- try {
111
- await config.setConfig(key, value);
112
- console.log(`Set ${key}`);
113
- flushStdioThenExit(0);
114
- } catch (e) {
115
- console.error("Error:", e.message);
116
- flushStdioThenExit(1);
117
- }
118
- });
119
-
120
- configCmd
121
- .command("get <key>")
122
- .description("Get a config value (sensitive keys are masked)")
123
- .action(async (key) => {
124
- try {
125
- const value = await config.getConfigValue(key);
126
- if (value === undefined || value === null) {
127
- console.log("(not set)");
128
- } else {
129
- console.log(config.maskValueForDisplay(key, value));
130
- }
131
- flushStdioThenExit(0);
132
- } catch (e) {
133
- console.error("Error:", e.message);
134
- flushStdioThenExit(1);
135
- }
136
- });
137
-
138
- configCmd
139
- .command("list")
140
- .description("List all config keys (values are hidden)")
141
- .action(async () => {
142
- try {
143
- const c = await config.listConfig();
144
- const keys = Object.keys(c);
145
- if (keys.length === 0) {
146
- console.log("No config set. Use: felo config set FELO_API_KEY <key>");
147
- } else {
148
- keys.forEach((k) => console.log(k));
149
- }
150
- flushStdioThenExit(0);
151
- } catch (e) {
152
- console.error("Error:", e.message);
153
- flushStdioThenExit(1);
154
- }
155
- });
156
-
157
- configCmd
158
- .command("unset <key>")
159
- .description("Remove a config value")
160
- .action(async (key) => {
161
- try {
162
- await config.unsetConfig(key);
163
- console.log(`Unset ${key}`);
164
- flushStdioThenExit(0);
165
- } catch (e) {
166
- console.error("Error:", e.message);
167
- flushStdioThenExit(1);
168
- }
169
- });
170
-
171
- configCmd
172
- .command("path")
173
- .description("Show config file path")
174
- .action(() => {
175
- console.log(config.getConfigPath());
176
- flushStdioThenExit(0);
177
- });
178
-
179
- program
180
- .command("web-extract")
181
- .description("Extract webpage content from a URL (markdown, text, or html)")
182
- .requiredOption("-u, --url <url>", "page URL to extract")
183
- .option(
184
- "-f, --format <format>",
185
- "output format: html, text, markdown",
186
- "markdown"
187
- )
188
- .option(
189
- "--target-selector <selector>",
190
- "CSS selector for target element only"
191
- )
192
- .option(
193
- "--wait-for-selector <selector>",
194
- "wait for selector before extracting"
195
- )
196
- .option("--readability", "use readability (main content only)")
197
- .option("--crawl-mode <mode>", "crawl mode: fast or fine", "fast")
198
- .option("-t, --timeout <seconds>", "request timeout in seconds", "60")
199
- .option("-j, --json", "output full API response as JSON")
200
- .action(async (opts) => {
201
- const timeoutMs = parseInt(opts.timeout, 10) * 1000;
202
- const code = await webExtract({
203
- url: opts.url,
204
- format: opts.format,
205
- targetSelector: opts.targetSelector,
206
- waitForSelector: opts.waitForSelector,
207
- readability: opts.readability,
208
- crawlMode: opts.crawlMode,
209
- timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
210
- json: opts.json,
211
- });
212
- process.exitCode = code;
213
- flushStdioThenExit(code);
214
- });
215
-
216
- program
217
- .command("youtube-subtitling")
218
- .description("Fetch YouTube video subtitles/captions by video URL or ID")
219
- .requiredOption("-v, --video-code <url-or-id>", "YouTube video URL or video ID (e.g. https://youtube.com/watch?v=ID)")
220
- .option("-l, --language <code>", "Subtitle language (e.g. en, zh-CN)")
221
- .option("--with-time", "Include start/duration per segment")
222
- .option("-j, --json", "Output full API response as JSON")
223
- .action(async (opts) => {
224
- const code = await youtubeSubtitling({
225
- videoCode: opts.videoCode,
226
- language: opts.language,
227
- withTime: opts.withTime,
228
- json: opts.json,
229
- });
230
- process.exitCode = code;
231
- flushStdioThenExit(code);
232
- });
233
-
234
- program
235
- .command("summarize")
236
- .description("Summarize text or URL (coming when API is available)")
237
- .argument("[input]", "text or URL to summarize")
238
- .action(() => {
239
- console.error("summarize: not yet implemented. Use felo search for now.");
240
- flushStdioThenExit(1);
241
- });
242
-
243
- program
244
- .command("translate")
245
- .description("Translate text (coming when API is available)")
246
- .argument("[text]", "text to translate")
247
- .action(() => {
248
- console.error("translate: not yet implemented. Use felo search for now.");
249
- flushStdioThenExit(1);
250
- });
251
-
252
- program.parse();
1
+ #!/usr/bin/env node
2
+
3
+ import { createRequire } from "module";
4
+ import { Command } from "commander";
5
+ import { search } from "./search.js";
6
+ import { slides } from "./slides.js";
7
+ import { superAgent } from "./superAgent.js";
8
+ import { webFetch } from "./webFetch.js";
9
+ import { youtubeSubtitling } from "./youtubeSubtitling.js";
10
+ import * as xSearch from "./xSearch.js";
11
+ import * as config from "./config.js";
12
+
13
+ const require = createRequire(import.meta.url);
14
+ const pkg = require("../package.json");
15
+
16
+ /** Delay (ms) before process.exit to let Windows libuv finish handle cleanup. */
17
+ const EXIT_DELAY_MS = 50;
18
+
19
+ /**
20
+ * Flush stdout then stderr, then exit after a short delay. Avoids Node.js
21
+ * Windows UV_HANDLE_CLOSING assertion when process.exit() runs while streams
22
+ * or other handles are still closing.
23
+ * @param {number} code - Exit code.
24
+ */
25
+ function flushStdioThenExit(code) {
26
+ const doExit = () => setTimeout(() => process.exit(code), EXIT_DELAY_MS);
27
+ const flushStderr = () => {
28
+ if (process.stderr?.writable && !process.stderr.destroyed) {
29
+ process.stderr.write("", () => doExit());
30
+ } else {
31
+ doExit();
32
+ }
33
+ };
34
+ if (process.stdout?.writable && !process.stdout.destroyed) {
35
+ process.stdout.write("", () => flushStderr());
36
+ } else {
37
+ flushStderr();
38
+ }
39
+ }
40
+
41
+ const program = new Command();
42
+
43
+ program
44
+ .name("felo")
45
+ .description("Felo AI CLI - real-time search from the terminal")
46
+ .version(pkg.version);
47
+
48
+ program
49
+ .command("search")
50
+ .description("Search for current information (weather, news, docs, etc.)")
51
+ .argument("<query>", "search query")
52
+ .option("-j, --json", "output raw JSON")
53
+ .option("-v, --verbose", "show query analysis and sources")
54
+ .option("-t, --timeout <seconds>", "request timeout in seconds", "60")
55
+ .action(async (query, opts) => {
56
+ const timeoutMs = parseInt(opts.timeout, 10) * 1000;
57
+ const code = await search(query, {
58
+ json: opts.json,
59
+ verbose: opts.verbose,
60
+ timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
61
+ });
62
+ process.exitCode = code;
63
+ flushStdioThenExit(code);
64
+ });
65
+
66
+ program
67
+ .command("slides")
68
+ .description(
69
+ "Generate PPT/slides from a prompt (async task, outputs live doc URL when done)"
70
+ )
71
+ .argument(
72
+ "<query>",
73
+ 'PPT generation prompt (e.g. "Felo, 2 pages" or "Introduction to React")'
74
+ )
75
+ .option("-j, --json", "output raw JSON with task_id and live_doc_url")
76
+ .option("-v, --verbose", "show polling status")
77
+ .option(
78
+ "-t, --timeout <seconds>",
79
+ "request timeout in seconds for each API call",
80
+ "60"
81
+ )
82
+ .option(
83
+ "--poll-timeout <seconds>",
84
+ "max seconds to wait for task completion",
85
+ "1200"
86
+ )
87
+ .action(async (query, opts) => {
88
+ const timeoutMs = parseInt(opts.timeout, 10) * 1000;
89
+ const pollTimeoutMs = parseInt(opts.pollTimeout, 10) * 1000 || 1_200_000;
90
+ const code = await slides(query, {
91
+ json: opts.json,
92
+ verbose: opts.verbose,
93
+ timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
94
+ pollTimeoutMs: Number.isNaN(pollTimeoutMs) ? 1_200_000 : pollTimeoutMs,
95
+ });
96
+ process.exitCode = code;
97
+ flushStdioThenExit(code);
98
+ });
99
+
100
+ program
101
+ .command("superagent")
102
+ .description(
103
+ "SuperAgent conversation with SSE streaming and LiveDoc (create + stream answer)"
104
+ )
105
+ .argument("<query>", "user query (1–2000 chars)")
106
+ .option("-j, --json", "output JSON with answer, thread_short_id, live_doc_short_id")
107
+ .option("-v, --verbose", "log stream key, thread ID, LiveDoc ID to stderr")
108
+ .option("-t, --timeout <seconds>", "request/stream timeout in seconds", "60")
109
+ .option("--live-doc-id <id>", "reuse existing LiveDoc short_id for continuous conversation")
110
+ .option("--thread-id <id>", "existing thread/conversation ID for follow-up questions")
111
+ .option("--accept-language <lang>", "language preference (e.g. zh, en)")
112
+ .action(async (query, opts) => {
113
+ const timeoutMs = parseInt(opts.timeout, 10) * 1000;
114
+ const code = await superAgent(query, {
115
+ json: opts.json,
116
+ verbose: opts.verbose,
117
+ timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
118
+ liveDocId: opts.liveDocId || undefined,
119
+ threadId: opts.threadId || undefined,
120
+ acceptLanguage: opts.acceptLanguage || undefined,
121
+ });
122
+ process.exitCode = code;
123
+ flushStdioThenExit(code);
124
+ });
125
+
126
+ const configCmd = program
127
+ .command("config")
128
+ .description(
129
+ "Manage persisted config (e.g. FELO_API_KEY). Stored in ~/.felo/config.json"
130
+ );
131
+
132
+ configCmd
133
+ .command("set <key> <value>")
134
+ .description(
135
+ "Set a config value (e.g. felo config set FELO_API_KEY your-key)"
136
+ )
137
+ .action(async (key, value) => {
138
+ try {
139
+ await config.setConfig(key, value);
140
+ console.log(`Set ${key}`);
141
+ flushStdioThenExit(0);
142
+ } catch (e) {
143
+ console.error("Error:", e.message);
144
+ flushStdioThenExit(1);
145
+ }
146
+ });
147
+
148
+ configCmd
149
+ .command("get <key>")
150
+ .description("Get a config value (sensitive keys are masked)")
151
+ .action(async (key) => {
152
+ try {
153
+ const value = await config.getConfigValue(key);
154
+ if (value === undefined || value === null) {
155
+ console.log("(not set)");
156
+ } else {
157
+ console.log(config.maskValueForDisplay(key, value));
158
+ }
159
+ flushStdioThenExit(0);
160
+ } catch (e) {
161
+ console.error("Error:", e.message);
162
+ flushStdioThenExit(1);
163
+ }
164
+ });
165
+
166
+ configCmd
167
+ .command("list")
168
+ .description("List all config keys (values are hidden)")
169
+ .action(async () => {
170
+ try {
171
+ const c = await config.listConfig();
172
+ const keys = Object.keys(c);
173
+ if (keys.length === 0) {
174
+ console.log("No config set. Use: felo config set FELO_API_KEY <key>");
175
+ } else {
176
+ keys.forEach((k) => console.log(k));
177
+ }
178
+ flushStdioThenExit(0);
179
+ } catch (e) {
180
+ console.error("Error:", e.message);
181
+ flushStdioThenExit(1);
182
+ }
183
+ });
184
+
185
+ configCmd
186
+ .command("unset <key>")
187
+ .description("Remove a config value")
188
+ .action(async (key) => {
189
+ try {
190
+ await config.unsetConfig(key);
191
+ console.log(`Unset ${key}`);
192
+ flushStdioThenExit(0);
193
+ } catch (e) {
194
+ console.error("Error:", e.message);
195
+ flushStdioThenExit(1);
196
+ }
197
+ });
198
+
199
+ configCmd
200
+ .command("path")
201
+ .description("Show config file path")
202
+ .action(() => {
203
+ console.log(config.getConfigPath());
204
+ flushStdioThenExit(0);
205
+ });
206
+
207
+ program
208
+ .command("web-fetch")
209
+ .description("Fetch webpage content from a URL (markdown, text, or html)")
210
+ .requiredOption("-u, --url <url>", "page URL to fetch")
211
+ .option(
212
+ "-f, --format <format>",
213
+ "output format: html, text, markdown",
214
+ "markdown"
215
+ )
216
+ .option(
217
+ "--target-selector <selector>",
218
+ "CSS selector for target element only"
219
+ )
220
+ .option(
221
+ "--wait-for-selector <selector>",
222
+ "wait for selector before fetching"
223
+ )
224
+ .option("--readability", "use readability (main content only)")
225
+ .option("--crawl-mode <mode>", "crawl mode: fast or fine", "fast")
226
+ .option("-t, --timeout <seconds>", "request timeout in seconds", "60")
227
+ .option("-j, --json", "output full API response as JSON")
228
+ .action(async (opts) => {
229
+ const timeoutMs = parseInt(opts.timeout, 10) * 1000;
230
+ const code = await webFetch({
231
+ url: opts.url,
232
+ format: opts.format,
233
+ targetSelector: opts.targetSelector,
234
+ waitForSelector: opts.waitForSelector,
235
+ readability: opts.readability,
236
+ crawlMode: opts.crawlMode,
237
+ timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
238
+ json: opts.json,
239
+ });
240
+ process.exitCode = code;
241
+ flushStdioThenExit(code);
242
+ });
243
+
244
+ program
245
+ .command("youtube-subtitling")
246
+ .description("Fetch YouTube video subtitles/captions by video URL or ID")
247
+ .requiredOption("-v, --video-code <url-or-id>", "YouTube video URL or video ID (e.g. https://youtube.com/watch?v=ID)")
248
+ .option("-l, --language <code>", "Subtitle language (e.g. en, zh-CN)")
249
+ .option("--with-time", "Include start/duration per segment")
250
+ .option("-j, --json", "Output full API response as JSON")
251
+ .action(async (opts) => {
252
+ const code = await youtubeSubtitling({
253
+ videoCode: opts.videoCode,
254
+ language: opts.language,
255
+ withTime: opts.withTime,
256
+ json: opts.json,
257
+ });
258
+ process.exitCode = code;
259
+ flushStdioThenExit(code);
260
+ });
261
+
262
+ // ── X Search ──
263
+ program
264
+ .command("x")
265
+ .description("Search X (Twitter) tweets, users, and replies")
266
+ .argument("[query]", "search keyword (default: search tweets)")
267
+ .option("-q, --query <text>", "search keyword (same as positional arg)")
268
+ .option("--id <values>", "tweet IDs or usernames (comma-separated)")
269
+ .option("--user", "switch to user mode")
270
+ .option("--tweets", "get user tweets (with --id --user)")
271
+ .option("-l, --limit <n>", "number of results to return")
272
+ .option("--cursor <cursor>", "pagination cursor")
273
+ .option("--include-replies", "include replies (with --tweets)")
274
+ .option("--query-type <type>", "query type filter (tweet search)")
275
+ .option("--since-time <val>", "start time filter")
276
+ .option("--until-time <val>", "end time filter")
277
+ .option("-j, --json", "output raw JSON")
278
+ .option("-t, --timeout <seconds>", "request timeout in seconds", "30")
279
+ .action(async (queryArg, opts) => {
280
+ const query = (queryArg || opts.query || "").trim();
281
+ const ids = opts.id
282
+ ? opts.id.split(",").map((s) => s.trim()).filter(Boolean)
283
+ : [];
284
+ const timeoutMs = parseInt(opts.timeout, 10) * 1000;
285
+ const commonOpts = {
286
+ json: opts.json,
287
+ timeoutMs: Number.isNaN(timeoutMs) ? 30000 : timeoutMs,
288
+ };
289
+ const limit = opts.limit ? parseInt(opts.limit, 10) : 0;
290
+ let code = 1;
291
+
292
+ if (query) {
293
+ // query mode
294
+ if (opts.user) {
295
+ // search users
296
+ code = await xSearch.userSearch(query, {
297
+ ...commonOpts,
298
+ cursor: opts.cursor,
299
+ });
300
+ } else {
301
+ // search tweets (default)
302
+ code = await xSearch.tweetSearch(query, {
303
+ ...commonOpts,
304
+ queryType: opts.queryType,
305
+ sinceTime: opts.sinceTime,
306
+ untilTime: opts.untilTime,
307
+ limit: limit || undefined,
308
+ cursor: opts.cursor,
309
+ });
310
+ }
311
+ } else if (ids.length) {
312
+ // id mode
313
+ if (opts.user) {
314
+ if (opts.tweets) {
315
+ // get user tweets
316
+ code = await xSearch.userTweets({
317
+ ...commonOpts,
318
+ username: ids[0],
319
+ limit: limit || undefined,
320
+ cursor: opts.cursor,
321
+ includeReplies: opts.includeReplies,
322
+ });
323
+ } else {
324
+ // get user info
325
+ code = await xSearch.userInfo(ids, commonOpts);
326
+ }
327
+ } else {
328
+ // get tweet replies (default for --id)
329
+ code = await xSearch.tweetReplies(ids, {
330
+ ...commonOpts,
331
+ cursor: opts.cursor,
332
+ sinceTime: opts.sinceTime ? parseInt(opts.sinceTime, 10) : undefined,
333
+ untilTime: opts.untilTime ? parseInt(opts.untilTime, 10) : undefined,
334
+ });
335
+ }
336
+ } else {
337
+ process.stderr.write(
338
+ 'Usage: felo x <query> or felo x --id <values>\n\n' +
339
+ 'Examples:\n' +
340
+ ' felo x "AI news" Search tweets\n' +
341
+ ' felo x "OpenAI" --user Search users\n' +
342
+ ' felo x --id "1234567890" Get tweet replies\n' +
343
+ ' felo x --id "elonmusk" --user Get user info\n' +
344
+ ' felo x --id "elonmusk" --user --tweets Get user tweets\n'
345
+ );
346
+ }
347
+
348
+ process.exitCode = code;
349
+ flushStdioThenExit(code);
350
+ });
351
+
352
+ program
353
+ .command("summarize")
354
+ .description("Summarize text or URL (coming when API is available)")
355
+ .argument("[input]", "text or URL to summarize")
356
+ .action(() => {
357
+ console.error("summarize: not yet implemented. Use felo search for now.");
358
+ flushStdioThenExit(1);
359
+ });
360
+
361
+ program
362
+ .command("translate")
363
+ .description("Translate text (coming when API is available)")
364
+ .argument("[text]", "text to translate")
365
+ .action(() => {
366
+ console.error("translate: not yet implemented. Use felo search for now.");
367
+ flushStdioThenExit(1);
368
+ });
369
+
370
+ program.parse();