felo-ai 0.2.25 → 0.2.30

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.
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "felo-ai",
3
+ "owner": {
4
+ "name": "Felo-Inc",
5
+ "url": "https://github.com/Felo-Inc"
6
+ },
7
+ "metadata": {
8
+ "description": "Felo AI skills for Claude Code — real-time search, PPT generation, SuperAgent, LiveDoc knowledge base, web fetch, YouTube subtitles, and X (Twitter) search",
9
+ "version": "1.0.0"
10
+ },
11
+ "plugins": [
12
+ {
13
+ "name": "felo-search",
14
+ "description": "Real-time web search powered by Felo AI",
15
+ "source": "./",
16
+ "strict": false,
17
+ "skills": [
18
+ "./felo-search"
19
+ ]
20
+ },
21
+ {
22
+ "name": "felo-livedoc",
23
+ "description": "Manage knowledge bases (LiveDocs) and semantic retrieval via Felo API",
24
+ "source": "./",
25
+ "strict": false,
26
+ "skills": [
27
+ "./felo-livedoc"
28
+ ]
29
+ },
30
+ {
31
+ "name": "felo-slides",
32
+ "description": "Generate PPT presentations from a prompt using Felo AI",
33
+ "source": "./",
34
+ "strict": false,
35
+ "skills": [
36
+ "./felo-slides"
37
+ ]
38
+ },
39
+ {
40
+ "name": "felo-superAgent",
41
+ "description": "SuperAgent conversation with SSE streaming and LiveDoc integration",
42
+ "source": "./",
43
+ "strict": false,
44
+ "skills": [
45
+ "./felo-superAgent"
46
+ ]
47
+ },
48
+ {
49
+ "name": "felo-web-fetch",
50
+ "description": "Fetch and extract webpage content in markdown, text, or HTML format",
51
+ "source": "./",
52
+ "strict": false,
53
+ "skills": [
54
+ "./felo-web-fetch"
55
+ ]
56
+ },
57
+ {
58
+ "name": "felo-youtube-subtitling",
59
+ "description": "Fetch YouTube video subtitles and captions by URL or video ID",
60
+ "source": "./",
61
+ "strict": false,
62
+ "skills": [
63
+ "./felo-youtube-subtitling"
64
+ ]
65
+ },
66
+ {
67
+ "name": "felo-x-search",
68
+ "description": "Search X (Twitter) tweets, users, and replies via Felo API",
69
+ "source": "./",
70
+ "strict": false,
71
+ "skills": [
72
+ "./felo-x-search"
73
+ ]
74
+ },
75
+ {
76
+ "name": "felo-content-to-slides",
77
+ "description": "Fetch content from a webpage or YouTube video and generate a PPT",
78
+ "source": "./",
79
+ "strict": false,
80
+ "skills": [
81
+ "./felo-content-to-slides"
82
+ ]
83
+ }
84
+ ]
85
+ }
package/CHANGELOG.md CHANGED
@@ -5,7 +5,15 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [0.2.24] - 2026-03-17
8
+ ## [0.2.29] - 2026-03-18
9
+
10
+ ### Fixed
11
+
12
+ - Fix spinner animation displaying repeated lines in non-TTY environments (e.g. Claude Code TUI, pipes, CI) by checking `process.stderr.isTTY` before writing `\r` control characters; spinner is silently skipped when stderr is not a TTY
13
+
14
+ ---
15
+
16
+ ## [0.2.25] - 2026-03-17
9
17
 
10
18
  ### Added
11
19
 
@@ -56,6 +56,7 @@ felo content-to-slides -v "https://www.youtube.com/watch?v=ID" [options]
56
56
  | `-l, --language <code>` | For --video: subtitle language (e.g. en, zh-CN) |
57
57
  | `-t, --timeout <seconds>` | Fetch timeout (default 60) |
58
58
  | `--poll-timeout <seconds>` | Max seconds to wait for PPT task (default 1200) |
59
+ | `--theme <id>` | PPT theme ID (list themes with `felo ppt-themes`) |
59
60
  | `-j, --json` | Output JSON with task_id and ppt/live_doc URL |
60
61
  | `--verbose` | Show polling status |
61
62
 
@@ -67,6 +68,9 @@ Provide **either** `--url` or `--video`, not both.
67
68
  # Web page → PPT (with readability)
68
69
  node src/cli.js content-to-slides --url "https://openclaw.ai/" --readability
69
70
 
71
+ # Web page → PPT with a specific theme
72
+ node src/cli.js content-to-slides --url "https://openclaw.ai/" --readability --theme "THEME_ID"
73
+
70
74
  # YouTube → PPT, with extra instruction
71
75
  node src/cli.js content-to-slides -v "https://www.youtube.com/watch?v=xxx" --extra-prompt "max 10 slides"
72
76
 
@@ -12,6 +12,7 @@ const SPINNER_INTERVAL_MS = 80;
12
12
  const STATUS_PAD = 56;
13
13
 
14
14
  function startSpinner(message) {
15
+ if (!process.stderr.isTTY) return null;
15
16
  const start = Date.now();
16
17
  let i = 0;
17
18
  const id = setInterval(() => {
@@ -25,7 +26,7 @@ function startSpinner(message) {
25
26
 
26
27
  function stopSpinner(id) {
27
28
  if (id != null) clearInterval(id);
28
- process.stderr.write(`\r${' '.repeat(STATUS_PAD)}\r`);
29
+ if (process.stderr.isTTY) process.stderr.write(`\r${' '.repeat(STATUS_PAD)}\r`);
29
30
  }
30
31
 
31
32
  function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
@@ -75,9 +75,22 @@ node felo-slides/scripts/run_ppt_task.mjs \
75
75
  --timeout 60
76
76
  ```
77
77
 
78
+ To apply a specific theme, first list available themes with `felo ppt-themes`, then pass the theme ID:
79
+
80
+ ```bash
81
+ node felo-slides/scripts/run_ppt_task.mjs \
82
+ --query "USER_PROMPT_HERE" \
83
+ --theme "THEME_ID_HERE" \
84
+ --interval 10 \
85
+ --max-wait 1800 \
86
+ --timeout 60
87
+ ```
88
+
78
89
  Script behavior:
79
90
 
80
91
  - Creates task via `POST https://openapi.felo.ai/v2/ppts`
92
+ - Supports optional `--theme <id>` to apply a PPT theme (sends `ppt_config.ai_theme_id`)
93
+ - Supports optional `--task-id <id>` to resume polling an existing task (skips creation)
81
94
  - Polls via `GET https://openapi.felo.ai/v2/tasks/{task_id}/historical`
82
95
  - Treats `COMPLETED`/`SUCCESS` as success terminal (case-insensitive)
83
96
  - Treats `FAILED`/`ERROR` as failure terminal
@@ -152,6 +165,14 @@ Timeout handling:
152
165
 
153
166
  - If timeout reached, return last known status and instruct user to retry later
154
167
  - Include `task_id` so user can query again
168
+ - **IMPORTANT**: To resume a timed-out task, use `--task-id` instead of `--query` to avoid creating a duplicate PPT:
169
+
170
+ ```bash
171
+ node felo-slides/scripts/run_ppt_task.mjs \
172
+ --task-id "TASK_ID_HERE" \
173
+ --interval 10 \
174
+ --max-wait 1800
175
+ ```
155
176
 
156
177
  ## Important Notes
157
178
 
@@ -12,7 +12,9 @@ function usage() {
12
12
  ' node felo-slides/scripts/run_ppt_task.mjs --query "your prompt" [options]',
13
13
  '',
14
14
  'Options:',
15
- ' --query <text> PPT prompt (required)',
15
+ ' --query <text> PPT prompt (required unless --task-id is given)',
16
+ ' --task-id <id> Resume polling an existing task (skip creation)',
17
+ ' --theme <id> PPT theme ID (from ppt-themes)',
16
18
  ' --interval <seconds> Poll interval, default 10',
17
19
  ' --max-wait <seconds> Max wait time, default 1800',
18
20
  ' --timeout <seconds> Request timeout, default 60',
@@ -26,6 +28,8 @@ function usage() {
26
28
  function parseArgs(argv) {
27
29
  const out = {
28
30
  query: '',
31
+ taskId: '',
32
+ theme: '',
29
33
  intervalSec: DEFAULT_INTERVAL_SEC,
30
34
  maxWaitSec: DEFAULT_MAX_WAIT_SEC,
31
35
  timeoutSec: DEFAULT_TIMEOUT_SEC,
@@ -44,6 +48,12 @@ function parseArgs(argv) {
44
48
  } else if (a === '--query') {
45
49
  out.query = argv[i + 1] ?? '';
46
50
  i += 1;
51
+ } else if (a === '--task-id') {
52
+ out.taskId = argv[i + 1] ?? '';
53
+ i += 1;
54
+ } else if (a === '--theme') {
55
+ out.theme = argv[i + 1] ?? '';
56
+ i += 1;
47
57
  } else if (a === '--interval') {
48
58
  out.intervalSec = Number.parseInt(argv[i + 1] ?? '', 10);
49
59
  i += 1;
@@ -128,7 +138,11 @@ function extractTaskUrls(historicalData, createData) {
128
138
  };
129
139
  }
130
140
 
131
- async function createTask(apiKey, apiBase, query, timeoutMs) {
141
+ async function createTask(apiKey, apiBase, query, timeoutMs, theme) {
142
+ const reqBody = { query };
143
+ if (theme) {
144
+ reqBody.ppt_config = { ai_theme_id: theme };
145
+ }
132
146
  const payload = await fetchJson(
133
147
  `${apiBase}/v2/ppts`,
134
148
  {
@@ -138,7 +152,7 @@ async function createTask(apiKey, apiBase, query, timeoutMs) {
138
152
  Authorization: `Bearer ${apiKey}`,
139
153
  'Content-Type': 'application/json',
140
154
  },
141
- body: JSON.stringify({ query }),
155
+ body: JSON.stringify(reqBody),
142
156
  },
143
157
  timeoutMs
144
158
  );
@@ -170,7 +184,7 @@ async function main() {
170
184
  usage();
171
185
  process.exit(0);
172
186
  }
173
- if (!args.query) {
187
+ if (!args.query && !args.taskId) {
174
188
  usage();
175
189
  process.exit(1);
176
190
  }
@@ -186,10 +200,22 @@ async function main() {
186
200
  const intervalMs = args.intervalSec * 1000;
187
201
  const maxWaitMs = args.maxWaitSec * 1000;
188
202
 
189
- const createData = await createTask(apiKey, apiBase, args.query, timeoutMs);
190
- const taskId = createData.task_id;
191
- if (args.verbose) {
192
- console.error(`Task ID: ${taskId}`);
203
+ let createData = {};
204
+ let taskId;
205
+
206
+ if (args.taskId) {
207
+ // Resume polling an existing task
208
+ taskId = args.taskId;
209
+ if (args.verbose) {
210
+ console.error(`Resuming task: ${taskId}`);
211
+ }
212
+ } else {
213
+ // Create a new task
214
+ createData = await createTask(apiKey, apiBase, args.query, timeoutMs, args.theme);
215
+ taskId = createData.task_id;
216
+ if (args.verbose) {
217
+ console.error(`Task ID: ${taskId}`);
218
+ }
193
219
  }
194
220
 
195
221
  const startAt = Date.now();
@@ -230,8 +230,7 @@ Examples:
230
230
  node felo-superAgent/scripts/run_superagent.mjs \
231
231
  --query "USER_QUERY_HERE" \
232
232
  --live-doc-id "LIVE_DOC_ID" \
233
- --accept-language en \
234
- --timeout 3600
233
+ --accept-language en
235
234
  ```
236
235
 
237
236
  **New conversation with skill ID (e.g., tweet writing):**
@@ -240,8 +239,7 @@ node felo-superAgent/scripts/run_superagent.mjs \
240
239
  --query "Write a tweet about the latest AI trends" \
241
240
  --live-doc-id "LIVE_DOC_ID" \
242
241
  --skill-id twitter-writer \
243
- --accept-language en \
244
- --timeout 3600
242
+ --accept-language en
245
243
  ```
246
244
 
247
245
  **Follow-up question (DEFAULT for 2nd+ messages):**
@@ -249,8 +247,7 @@ node felo-superAgent/scripts/run_superagent.mjs \
249
247
  node felo-superAgent/scripts/run_superagent.mjs \
250
248
  --query "USER_FOLLOW_UP_QUERY" \
251
249
  --thread-id "THREAD_SHORT_ID_FROM_PREVIOUS" \
252
- --live-doc-id "LIVE_DOC_ID" \
253
- --timeout 3600
250
+ --live-doc-id "LIVE_DOC_ID"
254
251
  ```
255
252
 
256
253
  ### Step 6: Extract State from stderr (Do NOT Re-output the Answer)
@@ -284,7 +281,7 @@ User: "What is quantum computing?"
284
281
  node felo-superAgent/scripts/run_superagent.mjs \
285
282
  --query "What is quantum computing?" \
286
283
  --live-doc-id "QPetunwpGnkKuZHStP7gwt" \
287
- --accept-language en --timeout 3600
284
+ --accept-language en
288
285
  ```
289
286
  **Step 6:** The answer is already streamed to the user. Extract from stderr `[state]` line: `thread_short_id = "CmYpuGwBgCnrUdDx5ZtmxA"`, `live_doc_id = "QPetunwpGnkKuZHStP7gwt"`. Do NOT repeat the answer.
290
287
 
@@ -299,8 +296,7 @@ User: "What are its practical applications?"
299
296
  node felo-superAgent/scripts/run_superagent.mjs \
300
297
  --query "What are its practical applications?" \
301
298
  --thread-id "CmYpuGwBgCnrUdDx5ZtmxA" \
302
- --live-doc-id "QPetunwpGnkKuZHStP7gwt" \
303
- --timeout 3600
299
+ --live-doc-id "QPetunwpGnkKuZHStP7gwt"
304
300
  ```
305
301
  **Step 6:** Answer already streamed. Extract updated `thread_short_id` from stderr `[state]` line (may be the same), keep `live_doc_id`.
306
302
 
@@ -326,7 +322,7 @@ node felo-superAgent/scripts/run_superagent.mjs \
326
322
  --query "Help me write a tweet about AI trends" \
327
323
  --live-doc-id "QPetunwpGnkKuZHStP7gwt" \
328
324
  --skill-id twitter-writer \
329
- --accept-language en --timeout 3600
325
+ --accept-language en
330
326
  ```
331
327
  **Step 6:** Answer already streamed. Extract new `thread_short_id` from stderr `[state]` line, keep same `live_doc_id`.
332
328
 
@@ -340,8 +336,7 @@ User: "Make it more casual and add some emojis"
340
336
  node felo-superAgent/scripts/run_superagent.mjs \
341
337
  --query "Make it more casual and add some emojis" \
342
338
  --thread-id "NEW_THREAD_FROM_TWEET" \
343
- --live-doc-id "QPetunwpGnkKuZHStP7gwt" \
344
- --timeout 3600
339
+ --live-doc-id "QPetunwpGnkKuZHStP7gwt"
345
340
  ```
346
341
 
347
342
  ### Example C: Logo Design
@@ -357,7 +352,7 @@ node felo-superAgent/scripts/run_superagent.mjs \
357
352
  --query "Design a logo for my coffee shop called Bean & Brew" \
358
353
  --live-doc-id "QPetunwpGnkKuZHStP7gwt" \
359
354
  --skill-id logo-and-branding \
360
- --accept-language en --timeout 3600
355
+ --accept-language en
361
356
  ```
362
357
 
363
358
  ### Example D: E-commerce Product Image
@@ -373,7 +368,7 @@ node felo-superAgent/scripts/run_superagent.mjs \
373
368
  --query "Generate a product image for a wireless headphone on white background" \
374
369
  --live-doc-id "QPetunwpGnkKuZHStP7gwt" \
375
370
  --skill-id ecommerce-product-image \
376
- --accept-language en --timeout 3600
371
+ --accept-language en
377
372
  ```
378
373
 
379
374
  ### Example E: User Requests a New Canvas
@@ -394,7 +389,6 @@ Extract new `live_doc_id`. Discard the old one. All subsequent calls use the new
394
389
  - `--query <text>` (REQUIRED) — User question, 1-2000 characters
395
390
  - `--live-doc-id <id>` (REQUIRED for new conversations) — LiveDoc ID (`live_doc_id`) to associate with
396
391
  - `--thread-id <id>` — Thread ID from previous response, for follow-up conversations
397
- - `--timeout <seconds>` — Request/stream timeout, default 3600 seconds
398
392
 
399
393
  **Skill parameters (new conversations only, ignored in follow-up):**
400
394
  - `--skill-id <id>` — Skill ID (see Constraint #8 for available skill IDs)
@@ -505,9 +499,8 @@ To use this skill, you need to set up your Felo API Key:
505
499
 
506
500
  ### Timeout Handling
507
501
 
508
- - Default timeout: 3600 seconds (recommended for all SuperAgent calls due to SSE streaming)
509
- - Idle timeout: 2 hours (no data received)
510
- - **Bash tool timeout:** MUST be set to at least 600000ms (10 minutes) when executing the script
502
+ - The SSE stream has its own idle timeout: 2 hours (no data received). The stream stays open as long as data keeps flowing.
503
+ - **Bash tool timeout:** MUST be set to at least 600000ms (10 minutes) when executing the script, because the SSE stream can run for a long time.
511
504
 
512
505
  ## Important Notes
513
506
 
@@ -561,3 +554,4 @@ Do NOT repeat or summarize the answer (already shown)
561
554
  - [SuperAgent API (Felo Open Platform)](https://openapi.felo.ai/docs/api-reference/v2/superagent.html)
562
555
  - [Felo Open Platform](https://openapi.felo.ai/docs/)
563
556
  - [Get API Key](https://felo.ai) (Settings -> API Keys)
557
+
@@ -7,6 +7,7 @@ const SPINNER_INTERVAL_MS = 80;
7
7
  const STATUS_PAD = 56;
8
8
 
9
9
  function startSpinner(message) {
10
+ if (!process.stderr.isTTY) return null;
10
11
  const start = Date.now();
11
12
  let i = 0;
12
13
  const id = setInterval(() => {
@@ -20,7 +21,7 @@ function startSpinner(message) {
20
21
 
21
22
  function stopSpinner(id) {
22
23
  if (id != null) clearInterval(id);
23
- process.stderr.write(`\r${' '.repeat(STATUS_PAD)}\r`);
24
+ if (process.stderr.isTTY) process.stderr.write(`\r${' '.repeat(STATUS_PAD)}\r`);
24
25
  }
25
26
 
26
27
  function usage() {
@@ -9,6 +9,7 @@ const SPINNER_INTERVAL_MS = 80;
9
9
  const STATUS_PAD = 56;
10
10
 
11
11
  function startSpinner(message) {
12
+ if (!process.stderr.isTTY) return null;
12
13
  const start = Date.now();
13
14
  let i = 0;
14
15
  const id = setInterval(() => {
@@ -22,7 +23,7 @@ function startSpinner(message) {
22
23
 
23
24
  function stopSpinner(id) {
24
25
  if (id != null) clearInterval(id);
25
- process.stderr.write(`\r${' '.repeat(STATUS_PAD)}\r`);
26
+ if (process.stderr.isTTY) process.stderr.write(`\r${' '.repeat(STATUS_PAD)}\r`);
26
27
  }
27
28
 
28
29
  function sleep(ms) {
@@ -7,6 +7,7 @@ const SPINNER_INTERVAL_MS = 80;
7
7
  const STATUS_PAD = 52;
8
8
 
9
9
  function startSpinner(message) {
10
+ if (!process.stderr.isTTY) return null;
10
11
  const start = Date.now();
11
12
  let i = 0;
12
13
  const id = setInterval(() => {
@@ -20,7 +21,7 @@ function startSpinner(message) {
20
21
 
21
22
  function stopSpinner(id) {
22
23
  if (id != null) clearInterval(id);
23
- process.stderr.write(`\r${' '.repeat(STATUS_PAD)}\r`);
24
+ if (process.stderr.isTTY) process.stderr.write(`\r${' '.repeat(STATUS_PAD)}\r`);
24
25
  }
25
26
 
26
27
  /** Extract video ID from YouTube URL or return string if plain ID. Returns null if invalid. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "felo-ai",
3
- "version": "0.2.25",
3
+ "version": "0.2.30",
4
4
  "description": "Felo AI CLI - real-time search, PPT generation, SuperAgent conversation, LiveDoc management, web fetch, YouTube subtitles, LiveDoc knowledge base, and X (Twitter) search from the terminal",
5
5
  "type": "module",
6
6
  "main": "src/cli.js",
package/src/cli.js CHANGED
@@ -3,7 +3,7 @@
3
3
  import { createRequire } from "module";
4
4
  import { Command } from "commander";
5
5
  import { search } from "./search.js";
6
- import { slides } from "./slides.js";
6
+ import { slides, listPptThemes } from "./slides.js";
7
7
  import { superAgent, listLiveDocs, listLiveDocResources } from "./superAgent.js";
8
8
  import { webFetch } from "./webFetch.js";
9
9
  import { youtubeSubtitling } from "./youtubeSubtitling.js";
@@ -71,7 +71,7 @@ program
71
71
  "Generate PPT/slides from a prompt (async task, outputs live doc URL when done)"
72
72
  )
73
73
  .argument(
74
- "<query>",
74
+ "[query]",
75
75
  'PPT generation prompt (e.g. "Felo, 2 pages" or "Introduction to React")'
76
76
  )
77
77
  .option("-j, --json", "output raw JSON with task_id and live_doc_url")
@@ -86,14 +86,49 @@ program
86
86
  "max seconds to wait for task completion",
87
87
  "1200"
88
88
  )
89
+ .option("--theme <id>", "PPT theme ID (from ppt-themes command)")
90
+ .option("--task-id <id>", "resume polling an existing task (skip creation)")
89
91
  .action(async (query, opts) => {
92
+ if (!query && !opts.taskId) {
93
+ console.error("Error: provide a <query> or --task-id to resume an existing task");
94
+ flushStdioThenExit(1);
95
+ return;
96
+ }
90
97
  const timeoutMs = parseInt(opts.timeout, 10) * 1000;
91
98
  const pollTimeoutMs = parseInt(opts.pollTimeout, 10) * 1000 || 1_200_000;
92
- const code = await slides(query, {
99
+ const pptConfig = opts.theme ? { ai_theme_id: opts.theme } : undefined;
100
+ const code = await slides(query || "", {
93
101
  json: opts.json,
94
102
  verbose: opts.verbose,
95
103
  timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
96
104
  pollTimeoutMs: Number.isNaN(pollTimeoutMs) ? 1_200_000 : pollTimeoutMs,
105
+ pptConfig,
106
+ taskId: opts.taskId,
107
+ });
108
+ process.exitCode = code;
109
+ flushStdioThenExit(code);
110
+ });
111
+
112
+ program
113
+ .command("ppt-themes")
114
+ .description("List available PPT themes with optional filtering")
115
+ .option("--lang <code>", "language code (e.g. en, zh-Hans)")
116
+ .option("--type <type>", "filter by theme type (e.g. default, custom)")
117
+ .option("-k, --keyword <keyword>", "search keyword for theme titles")
118
+ .option("-p, --page <number>", "page number (starting from 1)", "1")
119
+ .option("-s, --size <number>", "page size (max 100)", "20")
120
+ .option("-j, --json", "output raw JSON")
121
+ .option("-t, --timeout <seconds>", "request timeout in seconds", "60")
122
+ .action(async (opts) => {
123
+ const timeoutMs = parseInt(opts.timeout, 10) * 1000;
124
+ const code = await listPptThemes({
125
+ lang: opts.lang,
126
+ type: opts.type,
127
+ keyword: opts.keyword,
128
+ page: parseInt(opts.page, 10) || 1,
129
+ size: parseInt(opts.size, 10) || 20,
130
+ json: opts.json,
131
+ timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
97
132
  });
98
133
  process.exitCode = code;
99
134
  flushStdioThenExit(code);
@@ -341,6 +376,7 @@ program
341
376
  )
342
377
  .option("-j, --json", "Output JSON with task_id and ppt/live_doc URL")
343
378
  .option("--verbose", "Show polling status")
379
+ .option("--theme <id>", "PPT theme ID (from ppt-themes command)")
344
380
  .action(async (opts) => {
345
381
  const timeoutMs = parseInt(opts.timeout, 10) * 1000;
346
382
  const pollTimeoutMs = parseInt(opts.pollTimeout, 10) * 1000;
@@ -354,6 +390,7 @@ program
354
390
  pollTimeoutMs: Number.isNaN(pollTimeoutMs) ? 1_200_000 : pollTimeoutMs,
355
391
  json: opts.json,
356
392
  verbose: opts.verbose,
393
+ theme: opts.theme,
357
394
  });
358
395
  process.exitCode = code;
359
396
  flushStdioThenExit(code);
@@ -77,5 +77,6 @@ export async function contentToSlides(opts) {
77
77
  verbose: opts?.verbose,
78
78
  timeoutMs: fetchTimeoutMs,
79
79
  pollTimeoutMs: opts?.pollTimeoutMs,
80
+ pptConfig: opts?.theme ? { ai_theme_id: opts.theme } : undefined,
80
81
  });
81
82
  }
package/src/livedoc.js CHANGED
@@ -12,6 +12,7 @@ const STATUS_PAD = 56;
12
12
  // ── Shared helpers ──
13
13
 
14
14
  function startSpinner(message) {
15
+ if (!process.stderr.isTTY) return null;
15
16
  const start = Date.now();
16
17
  let i = 0;
17
18
  const id = setInterval(() => {
@@ -25,7 +26,7 @@ function startSpinner(message) {
25
26
 
26
27
  function stopSpinner(id) {
27
28
  if (id != null) clearInterval(id);
28
- process.stderr.write(`\r${' '.repeat(STATUS_PAD)}\r`);
29
+ if (process.stderr.isTTY) process.stderr.write(`\r${' '.repeat(STATUS_PAD)}\r`);
29
30
  }
30
31
 
31
32
  async function getApiBase() {
package/src/slides.js CHANGED
@@ -48,9 +48,18 @@ function normalizeTaskStatus(status) {
48
48
  /**
49
49
  * Create a PPT task. Returns { task_id, livedoc_short_id, ppt_business_id } or throws.
50
50
  * Uses fetchWithTimeoutAndRetry for 5xx retry (per PPT Task API error codes).
51
+ * @param {string} apiKey
52
+ * @param {string} query
53
+ * @param {number} timeoutMs
54
+ * @param {string} apiBase
55
+ * @param {{ ai_theme_id?: string }} [pptConfig]
51
56
  */
52
- async function createPptTask(apiKey, query, timeoutMs, apiBase) {
57
+ async function createPptTask(apiKey, query, timeoutMs, apiBase, pptConfig) {
53
58
  const url = `${apiBase}/v2/ppts`;
59
+ const body = { query: query.trim() };
60
+ if (pptConfig && Object.keys(pptConfig).length > 0) {
61
+ body.ppt_config = pptConfig;
62
+ }
54
63
  const res = await fetchWithTimeoutAndRetry(
55
64
  url,
56
65
  {
@@ -60,7 +69,7 @@ async function createPptTask(apiKey, query, timeoutMs, apiBase) {
60
69
  Authorization: `Bearer ${apiKey}`,
61
70
  "Content-Type": "application/json",
62
71
  },
63
- body: JSON.stringify({ query: query.trim() }),
72
+ body: JSON.stringify(body),
64
73
  },
65
74
  timeoutMs
66
75
  );
@@ -150,18 +159,30 @@ export async function slides(query, options = {}) {
150
159
  try {
151
160
  const apiBase = await getApiBase();
152
161
 
153
- process.stderr.write("Creating PPT task...\n");
162
+ let createResult = {};
163
+ let taskId;
154
164
 
155
- const createResult = await createPptTask(
156
- apiKey,
157
- query,
158
- requestTimeoutMs,
159
- apiBase
160
- );
161
- const taskId = createResult.task_id;
165
+ if (options.taskId) {
166
+ // Resume polling an existing task
167
+ taskId = options.taskId;
168
+ if (options.verbose || options.json) {
169
+ process.stderr.write(`Resuming task: ${taskId}\n`);
170
+ }
171
+ } else {
172
+ process.stderr.write("Creating PPT task...\n");
173
+
174
+ createResult = await createPptTask(
175
+ apiKey,
176
+ query,
177
+ requestTimeoutMs,
178
+ apiBase,
179
+ options.pptConfig
180
+ );
181
+ taskId = createResult.task_id;
162
182
 
163
- if (options.json && options.verbose) {
164
- process.stderr.write(`Task ID: ${taskId}\n`);
183
+ if (options.json && options.verbose) {
184
+ process.stderr.write(`Task ID: ${taskId}\n`);
185
+ }
165
186
  }
166
187
 
167
188
  // 默认显示 spinner 动画;仅在使用 -v/--json 时改为逐行状态输出
@@ -330,3 +351,79 @@ export async function slides(query, options = {}) {
330
351
  return 1;
331
352
  }
332
353
  }
354
+
355
+ /**
356
+ * List available PPT themes. Returns exit code (0 success, 1 failure).
357
+ * @param {Object} options - { lang?, type?, keyword?, page?, size?, json?, timeoutMs? }
358
+ */
359
+ export async function listPptThemes(options = {}) {
360
+ const apiKey = await getApiKey();
361
+ if (!apiKey) {
362
+ console.error(NO_KEY_MESSAGE.trim());
363
+ return 1;
364
+ }
365
+
366
+ const timeoutMs = options.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
367
+
368
+ try {
369
+ const apiBase = await getApiBase();
370
+ const params = new URLSearchParams();
371
+ if (options.lang) params.set("lang", options.lang);
372
+ if (options.type) params.set("type", options.type);
373
+ if (options.keyword) params.set("keyword", options.keyword);
374
+ if (options.page) params.set("page", String(options.page));
375
+ if (options.size) params.set("size", String(options.size));
376
+
377
+ const qs = params.toString();
378
+ const url = `${apiBase}/v2/ppt-themes${qs ? `?${qs}` : ""}`;
379
+
380
+ const res = await fetchWithTimeoutAndRetry(
381
+ url,
382
+ {
383
+ method: "GET",
384
+ headers: {
385
+ Accept: "application/json",
386
+ Authorization: `Bearer ${apiKey}`,
387
+ },
388
+ },
389
+ timeoutMs
390
+ );
391
+
392
+ const data = await res.json().catch(() => ({}));
393
+
394
+ if (data.status === "error") {
395
+ const msg = data.message || data.code || "Unknown error";
396
+ throw new Error(msg);
397
+ }
398
+
399
+ if (!res.ok) {
400
+ const msg =
401
+ data.message || data.error || res.statusText || `HTTP ${res.status}`;
402
+ throw new Error(msg);
403
+ }
404
+
405
+ const themes = data.data ?? [];
406
+
407
+ if (options.json) {
408
+ console.log(JSON.stringify(data, null, 2));
409
+ return 0;
410
+ }
411
+
412
+ if (!Array.isArray(themes) || themes.length === 0) {
413
+ console.log("No themes found.");
414
+ return 0;
415
+ }
416
+
417
+ for (const t of themes) {
418
+ console.log(`${t.id} ${t.title || "(untitled)"}`);
419
+ if (t.subtitle) console.log(` subtitle: ${t.subtitle}`);
420
+ if (t.description) console.log(` ${t.description}`);
421
+ console.log();
422
+ }
423
+
424
+ return 0;
425
+ } catch (err) {
426
+ console.error("Error:", err.message || err);
427
+ return 1;
428
+ }
429
+ }
package/src/webFetch.js CHANGED
@@ -8,6 +8,7 @@ const SPINNER_INTERVAL_MS = 80;
8
8
  const STATUS_PAD = 56;
9
9
 
10
10
  function startSpinner(message) {
11
+ if (!process.stderr.isTTY) return null;
11
12
  const start = Date.now();
12
13
  let i = 0;
13
14
  const id = setInterval(() => {
@@ -21,7 +22,7 @@ function startSpinner(message) {
21
22
 
22
23
  function stopSpinner(id) {
23
24
  if (id != null) clearInterval(id);
24
- process.stderr.write(`\r${' '.repeat(STATUS_PAD)}\r`);
25
+ if (process.stderr.isTTY) process.stderr.write(`\r${' '.repeat(STATUS_PAD)}\r`);
25
26
  }
26
27
 
27
28
  async function getApiBase() {
package/src/xSearch.js CHANGED
@@ -8,6 +8,7 @@ const SPINNER_INTERVAL_MS = 80;
8
8
  const STATUS_PAD = 56;
9
9
 
10
10
  function startSpinner(message) {
11
+ if (!process.stderr.isTTY) return null;
11
12
  const start = Date.now();
12
13
  let i = 0;
13
14
  const id = setInterval(() => {
@@ -21,7 +22,7 @@ function startSpinner(message) {
21
22
 
22
23
  function stopSpinner(id) {
23
24
  if (id != null) clearInterval(id);
24
- process.stderr.write(`\r${' '.repeat(STATUS_PAD)}\r`);
25
+ if (process.stderr.isTTY) process.stderr.write(`\r${' '.repeat(STATUS_PAD)}\r`);
25
26
  }
26
27
 
27
28
  async function getApiBase() {
@@ -8,6 +8,7 @@ const SPINNER_INTERVAL_MS = 80;
8
8
  const STATUS_PAD = 52;
9
9
 
10
10
  function startSpinner(message) {
11
+ if (!process.stderr.isTTY) return null;
11
12
  const start = Date.now();
12
13
  let i = 0;
13
14
  const id = setInterval(() => {
@@ -21,7 +22,7 @@ function startSpinner(message) {
21
22
 
22
23
  function stopSpinner(id) {
23
24
  if (id != null) clearInterval(id);
24
- process.stderr.write(`\r${' '.repeat(STATUS_PAD)}\r`);
25
+ if (process.stderr.isTTY) process.stderr.write(`\r${' '.repeat(STATUS_PAD)}\r`);
25
26
  }
26
27
 
27
28
  /** Extract video ID from a YouTube URL or return the string if it looks like a plain ID. Returns null if invalid. */