felo-ai 0.2.28 → 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.
- package/.claude-plugin/marketplace.json +85 -0
- package/CHANGELOG.md +8 -0
- package/felo-content-to-slides/SKILL.md +4 -0
- package/felo-livedoc/scripts/run_livedoc.mjs +2 -1
- package/felo-slides/SKILL.md +21 -0
- package/felo-slides/scripts/run_ppt_task.mjs +34 -8
- package/felo-web-fetch/scripts/run_web_fetch.mjs +2 -1
- package/felo-x-search/scripts/run_x_search.mjs +2 -1
- package/felo-youtube-subtitling/scripts/run_youtube_subtitling.mjs +2 -1
- package/package.json +1 -1
- package/src/cli.js +40 -3
- package/src/contentToSlides.js +1 -0
- package/src/livedoc.js +2 -1
- package/src/slides.js +109 -12
- package/src/webFetch.js +2 -1
- package/src/xSearch.js +2 -1
- package/src/youtubeSubtitling.js +2 -1
|
@@ -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,6 +5,14 @@ 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.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
|
+
|
|
8
16
|
## [0.2.25] - 2026-03-17
|
|
9
17
|
|
|
10
18
|
### Added
|
|
@@ -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)); }
|
package/felo-slides/SKILL.md
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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();
|
|
@@ -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.
|
|
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
|
-
"
|
|
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
|
|
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);
|
package/src/contentToSlides.js
CHANGED
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(
|
|
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
|
-
|
|
162
|
+
let createResult = {};
|
|
163
|
+
let taskId;
|
|
154
164
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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() {
|
package/src/youtubeSubtitling.js
CHANGED
|
@@ -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. */
|