felo-ai 0.2.14 → 0.2.16
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/CHANGELOG.md +19 -1
- package/felo-content-to-slides/README.md +78 -0
- package/felo-content-to-slides/SKILL.md +102 -0
- package/felo-livedoc/clawhub.json +12 -0
- package/felo-livedoc/scripts/run_livedoc.mjs +35 -4
- package/felo-superAgent/README.md +57 -13
- package/felo-superAgent/SKILL.md +5 -8
- package/felo-superAgent/clawhub.json +12 -0
- package/felo-x-search/clawhub.json +1 -1
- package/package.json +4 -2
- package/src/cli.js +100 -1
- package/src/contentToSlides.js +81 -0
- package/src/superAgent.js +193 -11
- package/src/webFetch.js +32 -0
- package/src/youtubeSubtitling.js +32 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,24 @@ 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.
|
|
8
|
+
## [0.2.14] - 2026-03-13
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`felo livedoc` command**: full CRUD for LiveDocs (knowledge bases) — `create`, `list`, `update`, `delete`
|
|
13
|
+
- **`felo livedoc` resource management**: `add-doc`, `add-urls`, `upload`, `resources`, `resource`, `remove-resource`
|
|
14
|
+
- **`felo livedoc retrieve <id>`**: semantic search across resources in a LiveDoc
|
|
15
|
+
- **`felo superagent` new options**: `--thread-id` (follow-up conversation), `--skill-id`, `--selected-resource-ids`, `--ext`
|
|
16
|
+
- **`FELO_API_BASE` config persistence**: support `felo config set FELO_API_BASE <url>`, priority: env > config > default
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- SuperAgent SSE `type=processing` events are now silently ignored
|
|
21
|
+
- Replaced all hardcoded Chinese strings with English in superAgent
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## [0.2.12] - 2026-03-10
|
|
9
26
|
|
|
10
27
|
Streamline the process and reduce the need for confirmation and selection.
|
|
11
28
|
|
|
@@ -38,6 +55,7 @@ Streamline the process and reduce the need for confirmation and selection.
|
|
|
38
55
|
|
|
39
56
|
Earlier releases: search, slides, web fetch, youtube-subtitling features.
|
|
40
57
|
|
|
58
|
+
[0.3.0]: https://github.com/Felo-Inc/felo-skills/compare/v0.2.10...v0.3.0
|
|
41
59
|
[0.2.10]: https://github.com/Felo-Inc/felo-skills/compare/v0.2.9...v0.2.10
|
|
42
60
|
[0.2.7]: https://github.com/Felo-Inc/felo-skills/compare/v0.2.6...v0.2.7
|
|
43
61
|
[0.2.6]: https://github.com/Felo-Inc/felo-skills/releases/tag/v0.2.6
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Felo Content to Slides Skill
|
|
2
|
+
|
|
3
|
+
Fetch content from a webpage or YouTube video, then generate a PPT from that content. Combines this repo's web-fetch, youtube-subtitling, and slides capabilities.
|
|
4
|
+
|
|
5
|
+
## Install Skill (Claude Code)
|
|
6
|
+
|
|
7
|
+
**One-line install:**
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx skills add Felo-Inc/felo-skills --skill felo-content-to-slides
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
**Manual install:** Copy this directory into Claude Code's skills folder:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Linux/macOS
|
|
17
|
+
cp -r felo-content-to-slides ~/.claude/skills/
|
|
18
|
+
|
|
19
|
+
# Windows (PowerShell)
|
|
20
|
+
Copy-Item -Recurse felo-content-to-slides "$env:USERPROFILE\.claude\skills\"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Notes
|
|
24
|
+
|
|
25
|
+
- **Generation time:** PPT generation usually takes **3–5 minutes** or more; please wait. Default max wait is 20 minutes (adjust with `--poll-timeout`).
|
|
26
|
+
|
|
27
|
+
## Configuration
|
|
28
|
+
|
|
29
|
+
Same as other Felo commands: set `FELO_API_KEY` (and optionally `FELO_API_BASE`).
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
felo config set FELO_API_KEY your-api-key-here
|
|
33
|
+
# or
|
|
34
|
+
export FELO_API_KEY="your-api-key-here" # Linux/macOS
|
|
35
|
+
$env:FELO_API_KEY="your-api-key-here" # Windows PowerShell
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Trigger
|
|
39
|
+
|
|
40
|
+
- Intent: "fetch page/video and make PPT", "turn URL into slides", "generate presentation from link"
|
|
41
|
+
- Explicit: `/felo-content-to-slides`
|
|
42
|
+
|
|
43
|
+
## Using the CLI
|
|
44
|
+
|
|
45
|
+
After installing **felo-ai**:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install -g felo-ai
|
|
49
|
+
felo content-to-slides -u "https://example.com/article" --readability
|
|
50
|
+
felo content-to-slides -v "https://www.youtube.com/watch?v=ID" --extra-prompt "max 10 slides"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
From this repo root (no global install):
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
node src/cli.js content-to-slides --url "https://openclaw.ai/" --readability
|
|
57
|
+
node src/cli.js content-to-slides --video "https://www.youtube.com/watch?v=xxx" -l zh-CN
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Options
|
|
61
|
+
|
|
62
|
+
| Option | Flag | Description |
|
|
63
|
+
|--------|------|-------------|
|
|
64
|
+
| Web page URL | `-u`, `--url` | URL to fetch and turn into slides |
|
|
65
|
+
| Video URL/ID | `-v`, `--video` | YouTube link or video ID (uses subtitles as content) |
|
|
66
|
+
| Extra instructions | `--extra-prompt` | e.g. "max 10 slides", "focus on conclusions" |
|
|
67
|
+
| Readability | `--readability` | For `--url` only: extract main body only |
|
|
68
|
+
| Subtitle language | `-l`, `--language` | For `--video` only: e.g. zh-CN, en |
|
|
69
|
+
| Fetch timeout | `-t`, `--timeout` | Seconds (default 60) |
|
|
70
|
+
| Poll timeout | `--poll-timeout` | Max seconds to wait for PPT (default 1200) |
|
|
71
|
+
| JSON output | `-j`, `--json` | Output task_id, ppt_url, live_doc_url |
|
|
72
|
+
| Verbose | `--verbose` | Print polling status |
|
|
73
|
+
|
|
74
|
+
## Links
|
|
75
|
+
|
|
76
|
+
- [SKILL.md](SKILL.md) – Full agent instructions
|
|
77
|
+
- [Felo Open Platform](https://openapi.felo.ai/docs/)
|
|
78
|
+
- [felo-web-fetch](../felo-web-fetch/) | [felo-youtube-subtitling](../felo-youtube-subtitling/) | [felo-slides](../felo-slides/)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: felo-content-to-slides
|
|
3
|
+
description: "Fetch content from a webpage or YouTube video, then generate a PPT from that content using Felo APIs. Use when the user wants to turn a URL (article/page or YouTube) into a presentation, or explicit /felo-content-to-slides."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Felo Content to Slides Skill
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
Trigger this skill when the user wants to:
|
|
11
|
+
|
|
12
|
+
- Turn a **webpage URL** or **article link** into a presentation
|
|
13
|
+
- Turn **YouTube video** (subtitles/transcript) into a PPT
|
|
14
|
+
- Fetch page/video content and generate slides from it
|
|
15
|
+
|
|
16
|
+
Trigger keywords (examples):
|
|
17
|
+
|
|
18
|
+
- Turn URL into slides, fetch page and make presentation, video transcript to PPT
|
|
19
|
+
- Explicit: `/felo-content-to-slides`, "use felo content to slides"
|
|
20
|
+
|
|
21
|
+
Do NOT use for:
|
|
22
|
+
|
|
23
|
+
- Only fetching content without PPT (use `felo-web-fetch` or `felo-youtube-subtitling`)
|
|
24
|
+
- Only generating PPT from a topic/outline (use `felo-slides`)
|
|
25
|
+
- Real-time search (use `felo-search`)
|
|
26
|
+
|
|
27
|
+
## Setup
|
|
28
|
+
|
|
29
|
+
Same as other Felo skills: set `FELO_API_KEY` (and optionally `FELO_API_BASE`). No extra setup.
|
|
30
|
+
|
|
31
|
+
## How to Execute
|
|
32
|
+
|
|
33
|
+
Use the **CLI** from this repo. From the **felo-skills repo root**:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
node src/cli.js content-to-slides --url "https://example.com/article" [options]
|
|
37
|
+
# or for YouTube:
|
|
38
|
+
node src/cli.js content-to-slides --video "https://www.youtube.com/watch?v=ID" [options]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
If **felo-ai** is installed globally (`npm install -g felo-ai`):
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
felo content-to-slides -u "https://example.com/article" [options]
|
|
45
|
+
felo content-to-slides -v "https://www.youtube.com/watch?v=ID" [options]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Options
|
|
49
|
+
|
|
50
|
+
| Option | Description |
|
|
51
|
+
|--------|-------------|
|
|
52
|
+
| `-u, --url <url>` | Web page URL to fetch and turn into slides |
|
|
53
|
+
| `-v, --video <url-or-id>` | YouTube video URL or ID (use subtitles as content) |
|
|
54
|
+
| `--extra-prompt <text>` | Extra instructions for PPT (e.g. max 10 slides) |
|
|
55
|
+
| `--readability` | For --url: use readability (main content only) |
|
|
56
|
+
| `-l, --language <code>` | For --video: subtitle language (e.g. en, zh-CN) |
|
|
57
|
+
| `-t, --timeout <seconds>` | Fetch timeout (default 60) |
|
|
58
|
+
| `--poll-timeout <seconds>` | Max seconds to wait for PPT task (default 1200) |
|
|
59
|
+
| `-j, --json` | Output JSON with task_id and ppt/live_doc URL |
|
|
60
|
+
| `--verbose` | Show polling status |
|
|
61
|
+
|
|
62
|
+
Provide **either** `--url` or `--video`, not both.
|
|
63
|
+
|
|
64
|
+
### Examples
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# Web page → PPT (with readability)
|
|
68
|
+
node src/cli.js content-to-slides --url "https://openclaw.ai/" --readability
|
|
69
|
+
|
|
70
|
+
# YouTube → PPT, with extra instruction
|
|
71
|
+
node src/cli.js content-to-slides -v "https://www.youtube.com/watch?v=xxx" --extra-prompt "max 10 slides"
|
|
72
|
+
|
|
73
|
+
# With JSON output
|
|
74
|
+
felo content-to-slides -u "https://example.com/article" --readability -j
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Output Format
|
|
78
|
+
|
|
79
|
+
On success:
|
|
80
|
+
|
|
81
|
+
```markdown
|
|
82
|
+
## Content to Slides Done
|
|
83
|
+
|
|
84
|
+
- Source: <webpage URL or YouTube video>
|
|
85
|
+
- Task ID: <task_id>
|
|
86
|
+
- PPT URL: <ppt_url>
|
|
87
|
+
- Live Doc: <live_doc_url or N/A>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
On failure: state which step failed (fetch / PPT generation) and the error message; suggest checking URL, network, or API key.
|
|
91
|
+
|
|
92
|
+
## Important Notes
|
|
93
|
+
|
|
94
|
+
- Always check `FELO_API_KEY` before running (or prompt user to run `felo config set FELO_API_KEY <key>`).
|
|
95
|
+
- For long articles or transcripts, content is truncated; if PPT create fails with size error, the user may retry with a shorter page or different URL.
|
|
96
|
+
- Web fetch may need `--timeout` for slow sites; YouTube may have no subtitles for some videos—report clearly.
|
|
97
|
+
|
|
98
|
+
## References
|
|
99
|
+
|
|
100
|
+
- Content fetch: felo-web-fetch, felo-youtube-subtitling (same repo)
|
|
101
|
+
- PPT generation: felo-slides (same repo)
|
|
102
|
+
- [Felo Open Platform](https://openapi.felo.ai/docs/)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Felo LiveDoc",
|
|
3
|
+
"tagline": "Manage knowledge bases (LiveDocs) and semantic retrieval via Felo API in Claude Code",
|
|
4
|
+
"description": "Felo LiveDoc lets you create, list, update, and delete knowledge bases (LiveDocs), add resources (text, URLs, file uploads), and run semantic retrieval over your knowledge base from Claude Code. Full CRUD for LiveDocs and resources. Requires a Felo API key.",
|
|
5
|
+
"category": "data",
|
|
6
|
+
"tags": ["felo", "livedoc", "knowledge-base", "semantic-search", "retrieval", "api", "documents"],
|
|
7
|
+
"version": "1.0.0",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"pricing": "free",
|
|
10
|
+
"support_url": "https://github.com/Felo-Inc/felo-skills/issues",
|
|
11
|
+
"homepage": "https://github.com/Felo-Inc/felo-skills"
|
|
12
|
+
}
|
|
@@ -143,7 +143,8 @@ function usage() {
|
|
|
143
143
|
' add-urls <short_id> Add URLs (--urls required, comma-separated, max 10)',
|
|
144
144
|
' upload <short_id> Upload file (--file required, --convert optional)',
|
|
145
145
|
' remove-resource <short_id> <resource_id> Delete a resource',
|
|
146
|
-
' retrieve <short_id> Semantic search (--query required)',
|
|
146
|
+
' retrieve <short_id> Semantic search (--query required, --resource-ids optional)',
|
|
147
|
+
' route <short_id> Route relevant resources by query (--query required)',
|
|
147
148
|
'',
|
|
148
149
|
'Options:',
|
|
149
150
|
' --name <name> LiveDoc name',
|
|
@@ -158,7 +159,9 @@ function usage() {
|
|
|
158
159
|
' --urls <urls> Comma-separated URLs',
|
|
159
160
|
' --file <path> File path to upload',
|
|
160
161
|
' --convert Convert uploaded file to document',
|
|
161
|
-
' --query <text> Retrieval query',
|
|
162
|
+
' --query <text> Retrieval/route query',
|
|
163
|
+
' --resource-ids <ids> Comma-separated resource IDs to search within (retrieve)',
|
|
164
|
+
' --max-resources <n> Max resources to return (route)',
|
|
162
165
|
' -j, --json Output raw JSON',
|
|
163
166
|
' -t, --timeout <ms> Timeout in ms (default: 60000)',
|
|
164
167
|
' --help Show this help',
|
|
@@ -168,7 +171,7 @@ function parseArgs(argv) {
|
|
|
168
171
|
const out = {
|
|
169
172
|
action: '', positional: [], name: '', description: '', icon: '',
|
|
170
173
|
keyword: '', page: '', size: '', type: '', content: '', title: '',
|
|
171
|
-
urls: '', file: '', convert: false, query: '',
|
|
174
|
+
urls: '', file: '', convert: false, query: '', resourceIds: '', maxResources: '',
|
|
172
175
|
json: false, timeoutMs: DEFAULT_TIMEOUT_MS, help: false,
|
|
173
176
|
};
|
|
174
177
|
const positional = [];
|
|
@@ -189,6 +192,8 @@ function parseArgs(argv) {
|
|
|
189
192
|
else if (a === '--urls') out.urls = argv[++i] || '';
|
|
190
193
|
else if (a === '--file') out.file = argv[++i] || '';
|
|
191
194
|
else if (a === '--query') out.query = argv[++i] || '';
|
|
195
|
+
else if (a === '--resource-ids') out.resourceIds = argv[++i] || '';
|
|
196
|
+
else if (a === '--max-resources') out.maxResources = argv[++i] || '';
|
|
192
197
|
else if (a === '-t' || a === '--timeout') {
|
|
193
198
|
const n = parseInt(argv[++i] || '', 10);
|
|
194
199
|
if (Number.isFinite(n) && n > 0) out.timeoutMs = n;
|
|
@@ -359,7 +364,11 @@ async function main() {
|
|
|
359
364
|
if (!shortId) { console.error('ERROR: short_id is required'); break; }
|
|
360
365
|
if (!args.query) { console.error('ERROR: --query is required'); break; }
|
|
361
366
|
spinnerId = startSpinner('Retrieving from knowledge base');
|
|
362
|
-
const
|
|
367
|
+
const body = { query: args.query };
|
|
368
|
+
if (args.resourceIds) {
|
|
369
|
+
body.resource_ids = args.resourceIds.split(',').map(id => id.trim()).filter(Boolean);
|
|
370
|
+
}
|
|
371
|
+
const payload = await apiRequest('POST', `/livedocs/${shortId}/resources/retrieve`, body, apiKey, apiBase, timeoutMs);
|
|
363
372
|
if (json) { console.log(JSON.stringify(payload, null, 2)); }
|
|
364
373
|
else {
|
|
365
374
|
const results = payload?.data || [];
|
|
@@ -372,6 +381,28 @@ async function main() {
|
|
|
372
381
|
code = 0;
|
|
373
382
|
break;
|
|
374
383
|
}
|
|
384
|
+
case 'route': {
|
|
385
|
+
if (!shortId) { console.error('ERROR: short_id is required'); break; }
|
|
386
|
+
if (!args.query) { console.error('ERROR: --query is required'); break; }
|
|
387
|
+
spinnerId = startSpinner('Routing relevant resources');
|
|
388
|
+
const body = { query: args.query };
|
|
389
|
+
if (args.maxResources) {
|
|
390
|
+
const n = parseInt(args.maxResources, 10);
|
|
391
|
+
if (Number.isFinite(n) && n > 0) body.max_resources = n;
|
|
392
|
+
}
|
|
393
|
+
const payload = await apiRequest('POST', `/livedocs/${shortId}/resources/route`, body, apiKey, apiBase, timeoutMs);
|
|
394
|
+
if (json) { console.log(JSON.stringify(payload, null, 2)); }
|
|
395
|
+
else {
|
|
396
|
+
const resourceIds = payload?.data || [];
|
|
397
|
+
if (!resourceIds.length) { process.stderr.write('No relevant resources found.\n'); }
|
|
398
|
+
else {
|
|
399
|
+
process.stdout.write(`Found ${resourceIds.length} relevant resource(s):\n\n`);
|
|
400
|
+
for (const id of resourceIds) process.stdout.write(`- ${id}\n`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
code = 0;
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
375
406
|
default:
|
|
376
407
|
console.error(`Unknown action: ${action}`);
|
|
377
408
|
usage();
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Felo SuperAgent Skill for Claude Code
|
|
2
2
|
|
|
3
|
-
**AI
|
|
3
|
+
**AI 对话与流式输出,支持连续会话。**
|
|
4
4
|
|
|
5
|
-
通过 Felo Open Platform 的 SuperAgent API,在 Claude Code 中发起与 SuperAgent 的对话、接收 SSE
|
|
5
|
+
通过 Felo Open Platform 的 SuperAgent API,在 Claude Code 中发起与 SuperAgent 的对话、接收 SSE 流式回复,并可查询会话详情。
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
|
|
11
11
|
- **流式对话**:创建会话后通过 SSE 实时接收 AI 回复
|
|
12
12
|
- **LiveDoc 关联**:每次会话对应一个 LiveDoc,可后续查看资源
|
|
13
|
-
- **连续对话**:通过 `
|
|
13
|
+
- **连续对话**:通过 `--thread-id` 在已有会话上继续提问
|
|
14
|
+
- **LiveDoc 管理**:列举 LiveDoc 列表、查看指定 LiveDoc 下的资源
|
|
14
15
|
- **多语言**:支持 `accept_language`(如 zh / en)
|
|
16
|
+
- **工具调用**:支持生图、研究报告、文档、PPT、HTML、Twitter 搜索等工具
|
|
15
17
|
|
|
16
18
|
**适用场景:**
|
|
17
19
|
|
|
@@ -24,6 +26,7 @@
|
|
|
24
26
|
- 仅需单次实时信息检索 → 使用 `felo-search`
|
|
25
27
|
- 仅需抓取网页正文 → 使用 `felo-web-fetch`
|
|
26
28
|
- 仅需生成 PPT → 使用 `felo-slides`
|
|
29
|
+
- 需要 LiveDoc 知识库功能 → 使用 `felo-livedoc`
|
|
27
30
|
|
|
28
31
|
---
|
|
29
32
|
|
|
@@ -80,19 +83,60 @@ node felo-superAgent/scripts/run_superagent.mjs --query "What is the latest news
|
|
|
80
83
|
|
|
81
84
|
输出为流式汇总后的完整回答正文。加 `--json` 可得到包含 `thread_short_id`、`live_doc_short_id` 的 JSON。
|
|
82
85
|
|
|
86
|
+
**CLI 命令(安装后):**
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# SuperAgent 对话
|
|
90
|
+
felo superagent "What is the latest news about AI?"
|
|
91
|
+
|
|
92
|
+
# 继续对话
|
|
93
|
+
felo superagent "Tell me more" --thread-id <thread_short_id>
|
|
94
|
+
|
|
95
|
+
# 列举 LiveDoc 列表
|
|
96
|
+
felo livedocs
|
|
97
|
+
felo livedocs --page 2 --size 10
|
|
98
|
+
felo livedocs --keyword AI
|
|
99
|
+
|
|
100
|
+
# 查看指定 LiveDoc 下的资源
|
|
101
|
+
felo livedoc-resources <livedoc-id>
|
|
102
|
+
```
|
|
103
|
+
|
|
83
104
|
---
|
|
84
105
|
|
|
85
106
|
## 脚本参数
|
|
86
107
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
|
90
|
-
|
|
|
91
|
-
| `--
|
|
92
|
-
| `--
|
|
93
|
-
| `--
|
|
94
|
-
| `--
|
|
95
|
-
| `--
|
|
108
|
+
### superagent
|
|
109
|
+
|
|
110
|
+
| 参数 | 说明 |
|
|
111
|
+
| --------------------------------- | ---------------------------------------------------------- |
|
|
112
|
+
| `--query <text>` | 用户问题(必填,1–2000 字符) |
|
|
113
|
+
| `--thread-id <id>` | 已有会话 ID,用于继续对话 |
|
|
114
|
+
| `--live-doc-id <id>` | 复用已有 LiveDoc short_id(连续对话) |
|
|
115
|
+
| `--skill-id <id>` | Skill ID(仅新建会话时有效) |
|
|
116
|
+
| `--selected-resource-ids <ids>` | 逗号分隔的资源 ID(仅新建会话时有效) |
|
|
117
|
+
| `--ext <json>` | 额外参数 JSON,如 `'{"style_id":"xxx"}'`(仅新建会话时有效)|
|
|
118
|
+
| `--accept-language <lang>` | 语言偏好,如 zh / en |
|
|
119
|
+
| `--timeout <seconds>` | 请求/流超时,默认 60 |
|
|
120
|
+
| `--json` | 输出 JSON(含 answer、thread_short_id、live_doc_short_id) |
|
|
121
|
+
| `--verbose` | 将流连接信息打到 stderr |
|
|
122
|
+
|
|
123
|
+
### livedocs
|
|
124
|
+
|
|
125
|
+
| 参数 | 说明 |
|
|
126
|
+
| ----------------------- | ------------------------ |
|
|
127
|
+
| `-p, --page <number>` | 页码,默认 1 |
|
|
128
|
+
| `-s, --size <number>` | 每页条数,默认 20 |
|
|
129
|
+
| `-k, --keyword <text>` | 关键词过滤 |
|
|
130
|
+
| `-j, --json` | 输出原始 JSON |
|
|
131
|
+
| `-t, --timeout <seconds>` | 请求超时,默认 60 |
|
|
132
|
+
|
|
133
|
+
### livedoc-resources
|
|
134
|
+
|
|
135
|
+
| 参数 | 说明 |
|
|
136
|
+
| -------------------------- | ------------------------ |
|
|
137
|
+
| `<livedoc-id>` | LiveDoc short_id(必填) |
|
|
138
|
+
| `-j, --json` | 输出原始 JSON |
|
|
139
|
+
| `-t, --timeout <seconds>` | 请求超时,默认 60 |
|
|
96
140
|
|
|
97
141
|
---
|
|
98
142
|
|
|
@@ -115,7 +159,7 @@ node felo-superAgent/scripts/run_superagent.mjs --query "What is the latest news
|
|
|
115
159
|
}
|
|
116
160
|
```
|
|
117
161
|
|
|
118
|
-
可用 `thread_short_id`
|
|
162
|
+
可用 `thread_short_id` 调用「查询会话详情」接口,`live_doc_short_id` 可传入 `felo-livedoc` 查询相关资源。
|
|
119
163
|
|
|
120
164
|
---
|
|
121
165
|
|
package/felo-superAgent/SKILL.md
CHANGED
|
@@ -10,9 +10,9 @@ description: "Felo SuperAgent API: AI conversation with real-time SSE streaming
|
|
|
10
10
|
Trigger this skill for:
|
|
11
11
|
|
|
12
12
|
- **SuperAgent 对话**:需要与 Felo SuperAgent 进行 AI 对话、流式输出
|
|
13
|
-
- **LiveDoc
|
|
14
|
-
-
|
|
15
|
-
- **多轮对话**:需要 thread_short_id / live_doc_short_id
|
|
13
|
+
- **LiveDoc 关联**:每次会话对应一个 LiveDoc,可后续查看资源
|
|
14
|
+
- **连续对话**:在已有 LiveDoc 上继续提问(传入 `live_doc_short_id`)
|
|
15
|
+
- **多轮对话**:需要 thread_short_id / live_doc_short_id 以便后续查询会话详情
|
|
16
16
|
|
|
17
17
|
**Trigger words / 触发词:**
|
|
18
18
|
|
|
@@ -25,6 +25,7 @@ Trigger this skill for:
|
|
|
25
25
|
- 简单单次问答、实时信息查询(优先用 `felo-search`)
|
|
26
26
|
- 仅需抓取网页内容(用 `felo-web-fetch`)
|
|
27
27
|
- 仅需生成 PPT(用 `felo-slides`)
|
|
28
|
+
- 需要 LiveDoc 知识库功能(用 `felo-livedoc`)
|
|
28
29
|
|
|
29
30
|
## Setup
|
|
30
31
|
|
|
@@ -83,8 +84,8 @@ node felo-superAgent/scripts/run_superagent.mjs \
|
|
|
83
84
|
|
|
84
85
|
Optional:
|
|
85
86
|
|
|
86
|
-
- **Reuse LiveDoc (连续对话):** `--live-doc-id "PvyKouzJirXjFdst4uKRK3"`
|
|
87
87
|
- **Language:** `--accept-language zh` or `--accept-language en`
|
|
88
|
+
- **Reuse LiveDoc (连续对话):** `--live-doc-id "PvyKouzJirXjFdst4uKRK3"`
|
|
88
89
|
- **JSON output:** `--json` (includes thread_short_id, live_doc_short_id, full answer)
|
|
89
90
|
- **Verbose:** `--verbose` (logs stream connection to stderr)
|
|
90
91
|
|
|
@@ -123,8 +124,6 @@ If the user asked for conversation detail or LiveDoc resources, you can call the
|
|
|
123
124
|
1. **POST** `/v2/conversations` → get `stream_key`, `thread_short_id`, `live_doc_short_id`
|
|
124
125
|
2. **GET** `/v2/conversations/stream/{stream_key}` → consume SSE until `done` or `error`
|
|
125
126
|
3. Optionally: **GET** `/v2/conversations/{thread_short_id}` → conversation detail
|
|
126
|
-
4. Optionally: **GET** `/v2/livedocs` → list LiveDocs
|
|
127
|
-
5. Optionally: **GET** `/v2/livedocs/{live_doc_short_id}/resources` → list resources
|
|
128
127
|
|
|
129
128
|
Base URL: `https://openapi.felo.ai` (override with `FELO_API_BASE` if needed).
|
|
130
129
|
|
|
@@ -135,8 +134,6 @@ Base URL: `https://openapi.felo.ai` (override with `FELO_API_BASE` if needed).
|
|
|
135
134
|
| INVALID_API_KEY | 401 | API Key 无效或已撤销 |
|
|
136
135
|
| SUPER_AGENT_CONVERSATION_CREATE_FAILED | 502 | 创建会话失败(下游错误) |
|
|
137
136
|
| SUPER_AGENT_CONVERSATION_QUERY_FAILED | 502 | 查询会话详情失败 |
|
|
138
|
-
| SUPER_AGENT_LIVEDOC_LIST_FAILED | 502 | 列举 LiveDocs 失败 |
|
|
139
|
-
| SUPER_AGENT_LIVEDOC_RESOURCES_FAILED | 502 | 列举 LiveDoc 资源失败 |
|
|
140
137
|
|
|
141
138
|
SSE stream may send:
|
|
142
139
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Felo SuperAgent",
|
|
3
|
+
"tagline": "AI conversation with real-time SSE streaming and LiveDoc in Claude Code",
|
|
4
|
+
"description": "Felo SuperAgent lets you chat with Felo SuperAgent via the Open API from Claude Code: create sessions, get SSE streamed replies, and continue conversations on the same LiveDoc. Supports thread/LiveDoc management, multi-language, and tools (image gen, reports, PPT, Twitter search). Requires a Felo API key.",
|
|
5
|
+
"category": "conversation",
|
|
6
|
+
"tags": ["felo", "superagent", "chat", "streaming", "livedoc", "api", "sse"],
|
|
7
|
+
"version": "1.0.0",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"pricing": "free",
|
|
10
|
+
"support_url": "https://github.com/Felo-Inc/felo-skills/issues",
|
|
11
|
+
"homepage": "https://github.com/Felo-Inc/felo-skills"
|
|
12
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"description": "Felo X Search lets you look up X (Twitter) user profiles, search users and tweets, fetch user timelines, and get tweet replies directly from Claude Code. Uses the Felo Open API; requires a Felo API key. Supports user lookup, user search, user tweets, tweet search, and tweet replies with readable Markdown or raw JSON output.",
|
|
5
5
|
"category": "data",
|
|
6
6
|
"tags": ["twitter", "x", "search", "social", "felo", "api"],
|
|
7
|
-
"version": "1.0.
|
|
7
|
+
"version": "1.0.1",
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"pricing": "free",
|
|
10
10
|
"support_url": "https://github.com/Felo-Inc/felo-skills/issues",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "felo-ai",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Felo AI CLI - real-time search, PPT generation, web fetch, YouTube subtitles, LiveDoc knowledge base from the terminal",
|
|
3
|
+
"version": "0.2.16",
|
|
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",
|
|
7
7
|
"bin": {
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
"felo-ai",
|
|
16
16
|
"search",
|
|
17
17
|
"slides",
|
|
18
|
+
"superagent",
|
|
19
|
+
"livedoc",
|
|
18
20
|
"web-fetch",
|
|
19
21
|
"youtube-subtitles",
|
|
20
22
|
"x-search",
|
package/src/cli.js
CHANGED
|
@@ -4,9 +4,10 @@ import { createRequire } from "module";
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { search } from "./search.js";
|
|
6
6
|
import { slides } from "./slides.js";
|
|
7
|
-
import { superAgent } from "./superAgent.js";
|
|
7
|
+
import { superAgent, listLiveDocs, listLiveDocResources } from "./superAgent.js";
|
|
8
8
|
import { webFetch } from "./webFetch.js";
|
|
9
9
|
import { youtubeSubtitling } from "./youtubeSubtitling.js";
|
|
10
|
+
import { contentToSlides } from "./contentToSlides.js";
|
|
10
11
|
import * as xSearch from "./xSearch.js";
|
|
11
12
|
import * as livedoc from "./livedoc.js";
|
|
12
13
|
import * as config from "./config.js";
|
|
@@ -109,8 +110,21 @@ program
|
|
|
109
110
|
.option("-t, --timeout <seconds>", "request/stream timeout in seconds", "60")
|
|
110
111
|
.option("--live-doc-id <id>", "reuse existing LiveDoc short_id for continuous conversation")
|
|
111
112
|
.option("--thread-id <id>", "existing thread/conversation ID for follow-up questions")
|
|
113
|
+
.option("--skill-id <id>", "skill ID (only for new conversations)")
|
|
114
|
+
.option("--selected-resource-ids <ids>", "comma-separated resource IDs (only for new conversations)")
|
|
115
|
+
.option("--ext <json>", "extra params as JSON string, e.g. '{\"style_id\":\"xxx\"}' (only for new conversations)")
|
|
112
116
|
.option("--accept-language <lang>", "language preference (e.g. zh, en)")
|
|
113
117
|
.action(async (query, opts) => {
|
|
118
|
+
let ext;
|
|
119
|
+
if (opts.ext) {
|
|
120
|
+
try {
|
|
121
|
+
ext = JSON.parse(opts.ext);
|
|
122
|
+
} catch {
|
|
123
|
+
console.error('Error: --ext must be valid JSON');
|
|
124
|
+
flushStdioThenExit(1);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
114
128
|
const timeoutMs = parseInt(opts.timeout, 10) * 1000;
|
|
115
129
|
const code = await superAgent(query, {
|
|
116
130
|
json: opts.json,
|
|
@@ -118,12 +132,52 @@ program
|
|
|
118
132
|
timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
|
|
119
133
|
liveDocId: opts.liveDocId || undefined,
|
|
120
134
|
threadId: opts.threadId || undefined,
|
|
135
|
+
skillId: opts.skillId || undefined,
|
|
136
|
+
selectedResourceIds: opts.selectedResourceIds ? opts.selectedResourceIds.split(',').map(s => s.trim()).filter(Boolean) : undefined,
|
|
137
|
+
ext,
|
|
121
138
|
acceptLanguage: opts.acceptLanguage || undefined,
|
|
122
139
|
});
|
|
123
140
|
process.exitCode = code;
|
|
124
141
|
flushStdioThenExit(code);
|
|
125
142
|
});
|
|
126
143
|
|
|
144
|
+
program
|
|
145
|
+
.command("livedocs")
|
|
146
|
+
.description("List LiveDocs with pagination and optional keyword filtering")
|
|
147
|
+
.option("-p, --page <number>", "page number", "1")
|
|
148
|
+
.option("-s, --size <number>", "page size", "20")
|
|
149
|
+
.option("-k, --keyword <keyword>", "keyword filter")
|
|
150
|
+
.option("-j, --json", "output raw JSON")
|
|
151
|
+
.option("-t, --timeout <seconds>", "request timeout in seconds", "60")
|
|
152
|
+
.action(async (opts) => {
|
|
153
|
+
const timeoutMs = parseInt(opts.timeout, 10) * 1000;
|
|
154
|
+
const code = await listLiveDocs({
|
|
155
|
+
page: parseInt(opts.page, 10) || 1,
|
|
156
|
+
size: parseInt(opts.size, 10) || 20,
|
|
157
|
+
keyword: opts.keyword || undefined,
|
|
158
|
+
json: opts.json,
|
|
159
|
+
timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
|
|
160
|
+
});
|
|
161
|
+
process.exitCode = code;
|
|
162
|
+
flushStdioThenExit(code);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
program
|
|
166
|
+
.command("livedoc-resources")
|
|
167
|
+
.description("List resources in a specific LiveDoc")
|
|
168
|
+
.argument("<livedoc-id>", "LiveDoc short_id")
|
|
169
|
+
.option("-j, --json", "output raw JSON")
|
|
170
|
+
.option("-t, --timeout <seconds>", "request timeout in seconds", "60")
|
|
171
|
+
.action(async (liveDocId, opts) => {
|
|
172
|
+
const timeoutMs = parseInt(opts.timeout, 10) * 1000;
|
|
173
|
+
const code = await listLiveDocResources(liveDocId, {
|
|
174
|
+
json: opts.json,
|
|
175
|
+
timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
|
|
176
|
+
});
|
|
177
|
+
process.exitCode = code;
|
|
178
|
+
flushStdioThenExit(code);
|
|
179
|
+
});
|
|
180
|
+
|
|
127
181
|
const configCmd = program
|
|
128
182
|
.command("config")
|
|
129
183
|
.description(
|
|
@@ -260,6 +314,51 @@ program
|
|
|
260
314
|
flushStdioThenExit(code);
|
|
261
315
|
});
|
|
262
316
|
|
|
317
|
+
program
|
|
318
|
+
.command("content-to-slides")
|
|
319
|
+
.description(
|
|
320
|
+
"Fetch content from a webpage or YouTube video, then generate a PPT from that content"
|
|
321
|
+
)
|
|
322
|
+
.option("-u, --url <url>", "Web page URL to fetch and turn into slides")
|
|
323
|
+
.option(
|
|
324
|
+
"-v, --video <url-or-id>",
|
|
325
|
+
"YouTube video URL or ID (use subtitles as content)"
|
|
326
|
+
)
|
|
327
|
+
.option(
|
|
328
|
+
"--extra-prompt <text>",
|
|
329
|
+
"Extra instructions for PPT (e.g. 10页以内)"
|
|
330
|
+
)
|
|
331
|
+
.option("--readability", "For --url: use readability (main content only)")
|
|
332
|
+
.option(
|
|
333
|
+
"-l, --language <code>",
|
|
334
|
+
"For --video: subtitle language (e.g. en, zh-CN)"
|
|
335
|
+
)
|
|
336
|
+
.option("-t, --timeout <seconds>", "Fetch timeout in seconds", "60")
|
|
337
|
+
.option(
|
|
338
|
+
"--poll-timeout <seconds>",
|
|
339
|
+
"Max seconds to wait for PPT task",
|
|
340
|
+
"1200"
|
|
341
|
+
)
|
|
342
|
+
.option("-j, --json", "Output JSON with task_id and ppt/live_doc URL")
|
|
343
|
+
.option("--verbose", "Show polling status")
|
|
344
|
+
.action(async (opts) => {
|
|
345
|
+
const timeoutMs = parseInt(opts.timeout, 10) * 1000;
|
|
346
|
+
const pollTimeoutMs = parseInt(opts.pollTimeout, 10) * 1000;
|
|
347
|
+
const code = await contentToSlides({
|
|
348
|
+
url: opts.url,
|
|
349
|
+
video: opts.video,
|
|
350
|
+
extraPrompt: opts.extraPrompt,
|
|
351
|
+
readability: opts.readability,
|
|
352
|
+
language: opts.language,
|
|
353
|
+
timeoutMs: Number.isNaN(timeoutMs) ? 60_000 : timeoutMs,
|
|
354
|
+
pollTimeoutMs: Number.isNaN(pollTimeoutMs) ? 1_200_000 : pollTimeoutMs,
|
|
355
|
+
json: opts.json,
|
|
356
|
+
verbose: opts.verbose,
|
|
357
|
+
});
|
|
358
|
+
process.exitCode = code;
|
|
359
|
+
flushStdioThenExit(code);
|
|
360
|
+
});
|
|
361
|
+
|
|
263
362
|
// ── X Search ──
|
|
264
363
|
program
|
|
265
364
|
.command("x")
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { getWebContent } from "./webFetch.js";
|
|
2
|
+
import { getYoutubeContent } from "./youtubeSubtitling.js";
|
|
3
|
+
import { slides } from "./slides.js";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_PROMPT_PREFIX =
|
|
6
|
+
"Generate a presentation from the content below. Summarize key points and organize into clear slides:";
|
|
7
|
+
const MAX_CONTENT_LENGTH = 20_000;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Fetch content from URL (web or YouTube), then generate PPT.
|
|
11
|
+
* @param {Object} opts - { url?, video?, extraPrompt?, readability?, language?, timeoutMs?, pollTimeoutMs?, json?, verbose? }
|
|
12
|
+
* @returns {Promise<number>} exit code (0 or 1)
|
|
13
|
+
*/
|
|
14
|
+
export async function contentToSlides(opts) {
|
|
15
|
+
const hasUrl = Boolean(opts?.url && String(opts.url).trim());
|
|
16
|
+
const hasVideo = Boolean(opts?.video && String(opts.video).trim());
|
|
17
|
+
if (!hasUrl && !hasVideo) {
|
|
18
|
+
process.stderr.write(
|
|
19
|
+
"ERROR: Provide either --url <page-url> or --video <youtube-url-or-id>\n"
|
|
20
|
+
);
|
|
21
|
+
return 1;
|
|
22
|
+
}
|
|
23
|
+
if (hasUrl && hasVideo) {
|
|
24
|
+
process.stderr.write("ERROR: Provide only one of --url or --video\n");
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const fetchTimeoutMs =
|
|
29
|
+
Number.isFinite(opts?.timeoutMs) && opts.timeoutMs > 0
|
|
30
|
+
? opts.timeoutMs
|
|
31
|
+
: 60_000;
|
|
32
|
+
|
|
33
|
+
let content;
|
|
34
|
+
try {
|
|
35
|
+
if (hasUrl) {
|
|
36
|
+
process.stderr.write(`Fetching page: ${opts.url.slice(0, 60)}...\n`);
|
|
37
|
+
content = await getWebContent({
|
|
38
|
+
url: opts.url,
|
|
39
|
+
format: "markdown",
|
|
40
|
+
readability: opts?.readability ?? true,
|
|
41
|
+
timeoutMs: fetchTimeoutMs,
|
|
42
|
+
});
|
|
43
|
+
} else {
|
|
44
|
+
process.stderr.write("Fetching YouTube subtitles...\n");
|
|
45
|
+
content = await getYoutubeContent({
|
|
46
|
+
videoCode: opts.video,
|
|
47
|
+
language: opts?.language ?? "",
|
|
48
|
+
timeoutMs: fetchTimeoutMs,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
} catch (err) {
|
|
52
|
+
process.stderr.write(`Fetch failed: ${err?.message || err}\n`);
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
content = (content || "").trim();
|
|
57
|
+
if (!content) {
|
|
58
|
+
process.stderr.write(
|
|
59
|
+
"ERROR: No content fetched. Page may be empty or video has no subtitles.\n"
|
|
60
|
+
);
|
|
61
|
+
return 1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (content.length > MAX_CONTENT_LENGTH) {
|
|
65
|
+
content =
|
|
66
|
+
content.slice(0, MAX_CONTENT_LENGTH) + "\n\n[... content truncated ...]";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const promptSuffix = opts?.extraPrompt
|
|
70
|
+
? `\n\nExtra instructions: ${opts.extraPrompt}`
|
|
71
|
+
: "";
|
|
72
|
+
const query = `${DEFAULT_PROMPT_PREFIX}\n\n${content}${promptSuffix}`;
|
|
73
|
+
|
|
74
|
+
process.stderr.write("Generating PPT (this may take a few minutes)...\n");
|
|
75
|
+
return slides(query, {
|
|
76
|
+
json: opts?.json,
|
|
77
|
+
verbose: opts?.verbose,
|
|
78
|
+
timeoutMs: fetchTimeoutMs,
|
|
79
|
+
pollTimeoutMs: opts?.pollTimeoutMs,
|
|
80
|
+
});
|
|
81
|
+
}
|
package/src/superAgent.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const DEFAULT_API_BASE = 'https://openapi.felo.ai';
|
|
2
2
|
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
3
|
-
/**
|
|
3
|
+
/** Stream idle timeout: disconnect if no data received for this duration (default 2 hours for long tasks like image generation) */
|
|
4
4
|
const STREAM_IDLE_TIMEOUT_MS = 2 * 60 * 60 * 1000;
|
|
5
5
|
/** Tools whose params and results should be silently ignored. */
|
|
6
6
|
const HIDDEN_TOOLS = new Set(['manage_outline']);
|
|
@@ -22,6 +22,18 @@ async function getApiKey() {
|
|
|
22
22
|
return typeof fromConfig === 'string' ? fromConfig.trim() : '';
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
async function getApiBase() {
|
|
26
|
+
if (process.env.FELO_API_BASE?.trim()) {
|
|
27
|
+
return process.env.FELO_API_BASE.trim().replace(/\/$/, '');
|
|
28
|
+
}
|
|
29
|
+
const { getConfigValue } = await import('./config.js');
|
|
30
|
+
const fromConfig = await getConfigValue('FELO_API_BASE');
|
|
31
|
+
if (typeof fromConfig === 'string' && fromConfig.trim()) {
|
|
32
|
+
return fromConfig.trim().replace(/\/$/, '');
|
|
33
|
+
}
|
|
34
|
+
return DEFAULT_API_BASE;
|
|
35
|
+
}
|
|
36
|
+
|
|
25
37
|
function getMessage(payload) {
|
|
26
38
|
return (
|
|
27
39
|
payload?.message ||
|
|
@@ -266,11 +278,11 @@ function extractToolResults(data) {
|
|
|
266
278
|
}
|
|
267
279
|
// Discovery (research report) tool results
|
|
268
280
|
if (t?.name === 'generate_discovery' && callResult?.status === 'success') {
|
|
269
|
-
out.push({ type: 'discovery', title: callResult?.title || t?.params?.title || '
|
|
281
|
+
out.push({ type: 'discovery', title: callResult?.title || t?.params?.title || 'Discovery' });
|
|
270
282
|
}
|
|
271
283
|
// Document generation tool results
|
|
272
284
|
if (t?.name === 'generate_document' && callResult?.status === 'success') {
|
|
273
|
-
out.push({ type: 'document', title: callResult?.title || t?.params?.title || '
|
|
285
|
+
out.push({ type: 'document', title: callResult?.title || t?.params?.title || 'Document' });
|
|
274
286
|
}
|
|
275
287
|
// PPT generation tool results
|
|
276
288
|
if (t?.name === 'generate_ppt' && callResult?.status === 'success') {
|
|
@@ -350,9 +362,9 @@ function dispatch(eventType, dataStr, onMessage, onError, onDone, onEvent, onToo
|
|
|
350
362
|
const text = data?.content ?? data?.text ?? data?.delta;
|
|
351
363
|
if (typeof text === 'string') onMessage(text);
|
|
352
364
|
} else if (type === 'message' && onStatusMessage && data?.query) {
|
|
353
|
-
onStatusMessage(
|
|
354
|
-
} else if (type === 'processing'
|
|
355
|
-
|
|
365
|
+
onStatusMessage(`Received: ${data.query}`);
|
|
366
|
+
} else if (type === 'processing') {
|
|
367
|
+
// Silently ignore processing events
|
|
356
368
|
} else if (type === 'tools' && onToolCall) {
|
|
357
369
|
const params = extractToolParams(data);
|
|
358
370
|
for (const item of params) onToolCall(item);
|
|
@@ -379,6 +391,160 @@ function dispatch(eventType, dataStr, onMessage, onError, onDone, onEvent, onToo
|
|
|
379
391
|
}
|
|
380
392
|
}
|
|
381
393
|
|
|
394
|
+
/**
|
|
395
|
+
* List LiveDocs with pagination and optional keyword filtering.
|
|
396
|
+
* @param {Object} [options]
|
|
397
|
+
* @param {number} [options.page] - Page number (default 1).
|
|
398
|
+
* @param {number} [options.size] - Page size (default 20).
|
|
399
|
+
* @param {string} [options.keyword] - Keyword filter.
|
|
400
|
+
* @param {boolean} [options.json] - Output raw JSON.
|
|
401
|
+
* @param {number} [options.timeoutMs] - Request timeout in ms.
|
|
402
|
+
* @returns {Promise<number>} Exit code 0 or 1.
|
|
403
|
+
*/
|
|
404
|
+
export async function listLiveDocs(options = {}) {
|
|
405
|
+
const apiKey = await getApiKey();
|
|
406
|
+
if (!apiKey) {
|
|
407
|
+
process.stderr.write(NO_KEY_MESSAGE.trim() + '\n');
|
|
408
|
+
return 1;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const apiBase = await getApiBase();
|
|
412
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
413
|
+
const page = options.page ?? 1;
|
|
414
|
+
const size = options.size ?? 20;
|
|
415
|
+
|
|
416
|
+
const params = new URLSearchParams();
|
|
417
|
+
params.set('page', String(page));
|
|
418
|
+
params.set('size', String(size));
|
|
419
|
+
if (options.keyword) params.set('keyword', options.keyword);
|
|
420
|
+
|
|
421
|
+
const url = `${apiBase}/v2/livedocs?${params.toString()}`;
|
|
422
|
+
|
|
423
|
+
const controller = new AbortController();
|
|
424
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
const res = await fetch(url, {
|
|
428
|
+
method: 'GET',
|
|
429
|
+
headers: {
|
|
430
|
+
Accept: 'application/json',
|
|
431
|
+
Authorization: `Bearer ${apiKey}`,
|
|
432
|
+
},
|
|
433
|
+
signal: controller.signal,
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
let data = {};
|
|
437
|
+
try {
|
|
438
|
+
data = await res.json();
|
|
439
|
+
} catch {
|
|
440
|
+
data = {};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (!res.ok) {
|
|
444
|
+
throw new Error(`HTTP ${res.status}: ${getMessage(data)}`);
|
|
445
|
+
}
|
|
446
|
+
if (isApiError(data)) {
|
|
447
|
+
throw new Error(getMessage(data));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const payload = data?.data ?? {};
|
|
451
|
+
const total = payload.total ?? 0;
|
|
452
|
+
const items = payload.items ?? [];
|
|
453
|
+
|
|
454
|
+
if (options.json) {
|
|
455
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
456
|
+
} else {
|
|
457
|
+
const isDev = apiBase.includes('-dev');
|
|
458
|
+
const webHost = isDev ? 'https://dev.felo.ai' : 'https://felo.ai';
|
|
459
|
+
const totalPages = Math.ceil(total / size);
|
|
460
|
+
console.log(`LiveDocs (total: ${total}, page ${page}/${totalPages})\n`);
|
|
461
|
+
for (const item of items) {
|
|
462
|
+
const shortId = item.short_id || '(no ID)';
|
|
463
|
+
console.log(`${webHost}/livedoc/${shortId}`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return 0;
|
|
468
|
+
} catch (err) {
|
|
469
|
+
const msg = err?.message || err;
|
|
470
|
+
process.stderr.write(`Error: ${msg}\n`);
|
|
471
|
+
return 1;
|
|
472
|
+
} finally {
|
|
473
|
+
clearTimeout(timer);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* List resources in a specific LiveDoc.
|
|
479
|
+
* @param {string} liveDocId - LiveDoc short_id.
|
|
480
|
+
* @param {Object} [options]
|
|
481
|
+
* @param {boolean} [options.json] - Output raw JSON.
|
|
482
|
+
* @param {number} [options.timeoutMs] - Request timeout in ms.
|
|
483
|
+
* @returns {Promise<number>} Exit code 0 or 1.
|
|
484
|
+
*/
|
|
485
|
+
export async function listLiveDocResources(liveDocId, options = {}) {
|
|
486
|
+
const apiKey = await getApiKey();
|
|
487
|
+
if (!apiKey) {
|
|
488
|
+
process.stderr.write(NO_KEY_MESSAGE.trim() + '\n');
|
|
489
|
+
return 1;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const apiBase = await getApiBase();
|
|
493
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
494
|
+
const url = `${apiBase}/v2/livedocs/${encodeURIComponent(liveDocId)}/resources`;
|
|
495
|
+
|
|
496
|
+
const controller = new AbortController();
|
|
497
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
498
|
+
|
|
499
|
+
try {
|
|
500
|
+
const res = await fetch(url, {
|
|
501
|
+
method: 'GET',
|
|
502
|
+
headers: {
|
|
503
|
+
Accept: 'application/json',
|
|
504
|
+
Authorization: `Bearer ${apiKey}`,
|
|
505
|
+
},
|
|
506
|
+
signal: controller.signal,
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
let data = {};
|
|
510
|
+
try {
|
|
511
|
+
data = await res.json();
|
|
512
|
+
} catch {
|
|
513
|
+
data = {};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (!res.ok) {
|
|
517
|
+
throw new Error(`HTTP ${res.status}: ${getMessage(data)}`);
|
|
518
|
+
}
|
|
519
|
+
if (isApiError(data)) {
|
|
520
|
+
throw new Error(getMessage(data));
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const payload = data?.data ?? {};
|
|
524
|
+
const total = payload.total ?? 0;
|
|
525
|
+
const items = payload.items ?? [];
|
|
526
|
+
|
|
527
|
+
if (options.json) {
|
|
528
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
529
|
+
} else {
|
|
530
|
+
console.log(`Resources (total: ${total})\n`);
|
|
531
|
+
for (const item of items) {
|
|
532
|
+
const title = item.title || '(no title)';
|
|
533
|
+
const id = item.id || '(no ID)';
|
|
534
|
+
console.log(`[${title}] ${id}`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return 0;
|
|
539
|
+
} catch (err) {
|
|
540
|
+
const msg = err?.message || err;
|
|
541
|
+
process.stderr.write(`Error: ${msg}\n`);
|
|
542
|
+
return 1;
|
|
543
|
+
} finally {
|
|
544
|
+
clearTimeout(timer);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
382
548
|
/**
|
|
383
549
|
* Run SuperAgent: create conversation, consume SSE stream, output answer.
|
|
384
550
|
* @param {string} query - User query (1–2000 chars).
|
|
@@ -388,6 +554,9 @@ function dispatch(eventType, dataStr, onMessage, onError, onDone, onEvent, onToo
|
|
|
388
554
|
* @param {number} [options.timeoutMs] - Request/stream timeout in ms.
|
|
389
555
|
* @param {string} [options.liveDocId] - Reuse existing LiveDoc short_id.
|
|
390
556
|
* @param {string} [options.threadId] - Existing thread/conversation ID for follow-up.
|
|
557
|
+
* @param {string} [options.skillId] - Skill ID (only for new conversations).
|
|
558
|
+
* @param {string[]} [options.selectedResourceIds] - Resource IDs (only for new conversations).
|
|
559
|
+
* @param {Object} [options.ext] - Extra params object (only for new conversations).
|
|
391
560
|
* @param {string} [options.acceptLanguage] - e.g. zh, en.
|
|
392
561
|
* @returns {Promise<number>} Exit code 0 or 1.
|
|
393
562
|
*/
|
|
@@ -398,7 +567,7 @@ export async function superAgent(query, options = {}) {
|
|
|
398
567
|
return 1;
|
|
399
568
|
}
|
|
400
569
|
|
|
401
|
-
const apiBase =
|
|
570
|
+
const apiBase = await getApiBase();
|
|
402
571
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
403
572
|
const body = {
|
|
404
573
|
query: String(query).trim().slice(0, 2000),
|
|
@@ -408,6 +577,19 @@ export async function superAgent(query, options = {}) {
|
|
|
408
577
|
|
|
409
578
|
try {
|
|
410
579
|
const threadId = options.threadId;
|
|
580
|
+
|
|
581
|
+
// skill_id, selected_resource_ids, ext only supported for new conversations
|
|
582
|
+
const createOnlyParams = ['skillId', 'selectedResourceIds', 'ext'];
|
|
583
|
+
const hasCreateOnlyParams = createOnlyParams.some(k => options[k] !== undefined);
|
|
584
|
+
if (threadId && hasCreateOnlyParams) {
|
|
585
|
+
process.stderr.write('Warning: --skill-id, --selected-resource-ids, --ext are only supported for new conversations, ignored in follow-up mode.\n');
|
|
586
|
+
}
|
|
587
|
+
if (!threadId) {
|
|
588
|
+
if (options.skillId) body.skill_id = options.skillId;
|
|
589
|
+
if (options.selectedResourceIds) body.selected_resource_ids = options.selectedResourceIds;
|
|
590
|
+
if (options.ext) body.ext = options.ext;
|
|
591
|
+
}
|
|
592
|
+
|
|
411
593
|
process.stderr.write(threadId ? 'SuperAgent: following up...\n' : 'SuperAgent: creating conversation...\n');
|
|
412
594
|
|
|
413
595
|
const createData = await createConversation(apiKey, apiBase, body, timeoutMs, threadId);
|
|
@@ -468,7 +650,7 @@ export async function superAgent(query, options = {}) {
|
|
|
468
650
|
if (isJson) return;
|
|
469
651
|
if (item.type === 'image') {
|
|
470
652
|
if (liveDocUrl) {
|
|
471
|
-
console.log(`[${item.title || '
|
|
653
|
+
console.log(`[${item.title || 'Image'}](${liveDocUrl})`);
|
|
472
654
|
} else {
|
|
473
655
|
console.log(item.image_url);
|
|
474
656
|
}
|
|
@@ -480,9 +662,9 @@ export async function superAgent(query, options = {}) {
|
|
|
480
662
|
}
|
|
481
663
|
} else if (item.type === 'document') {
|
|
482
664
|
if (liveDocUrl) {
|
|
483
|
-
console.log(`[${item.title || '
|
|
665
|
+
console.log(`[${item.title || 'Document'}](${liveDocUrl})`);
|
|
484
666
|
} else {
|
|
485
|
-
console.log(item.title || '
|
|
667
|
+
console.log(item.title || 'Document');
|
|
486
668
|
}
|
|
487
669
|
} else if (item.type === 'ppt') {
|
|
488
670
|
if (liveDocUrl) {
|
|
@@ -601,7 +783,7 @@ export async function superAgent(query, options = {}) {
|
|
|
601
783
|
process.stderr.write(`Error: ${msg}\n`);
|
|
602
784
|
if (String(msg).toLowerCase().includes('stream error')) {
|
|
603
785
|
process.stderr.write(
|
|
604
|
-
'
|
|
786
|
+
'(Stream idle disconnect — likely a proxy/firewall closing the connection during long tasks like image generation. Try a direct connection or increase proxy idle timeout.)\n'
|
|
605
787
|
);
|
|
606
788
|
}
|
|
607
789
|
return 1;
|
package/src/webFetch.js
CHANGED
|
@@ -146,3 +146,35 @@ export async function webFetch(opts) {
|
|
|
146
146
|
stopSpinner(spinnerId);
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Fetch webpage content and return as string (for content-to-slides). Does not print.
|
|
152
|
+
* @param {Object} opts - { url, format, readability, timeoutMs }
|
|
153
|
+
* @returns {Promise<string>} content or throws
|
|
154
|
+
*/
|
|
155
|
+
export async function getWebContent(opts) {
|
|
156
|
+
const apiKey = await getApiKey();
|
|
157
|
+
if (!apiKey) throw new Error(NO_KEY_MESSAGE.trim());
|
|
158
|
+
if (!opts?.url || typeof opts.url !== 'string' || !opts.url.trim()) {
|
|
159
|
+
throw new Error('URL is required');
|
|
160
|
+
}
|
|
161
|
+
const apiBase = await getApiBase();
|
|
162
|
+
const timeoutMs = Number.isFinite(opts?.timeoutMs) && opts.timeoutMs > 0
|
|
163
|
+
? opts.timeoutMs
|
|
164
|
+
: DEFAULT_TIMEOUT_MS;
|
|
165
|
+
const body = {
|
|
166
|
+
url: opts.url,
|
|
167
|
+
output_format: opts.format || 'markdown',
|
|
168
|
+
crawl_mode: opts.crawlMode || 'fast',
|
|
169
|
+
with_readability: Boolean(opts.readability),
|
|
170
|
+
timeout: timeoutMs,
|
|
171
|
+
};
|
|
172
|
+
if (opts.targetSelector) body.target_selector = opts.targetSelector;
|
|
173
|
+
if (opts.waitForSelector) body.wait_for_selector = opts.waitForSelector;
|
|
174
|
+
const payload = await fetchContent(apiBase, apiKey, body, timeoutMs);
|
|
175
|
+
const out = stringifyContent(payload?.data?.content);
|
|
176
|
+
if (out == null || String(out).trim() === '') {
|
|
177
|
+
throw new Error(`No content fetched from ${opts.url}`);
|
|
178
|
+
}
|
|
179
|
+
return out;
|
|
180
|
+
}
|
package/src/youtubeSubtitling.js
CHANGED
|
@@ -177,3 +177,35 @@ export async function youtubeSubtitling(opts) {
|
|
|
177
177
|
stopSpinner(spinnerId);
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Fetch YouTube subtitles and return as string (for content-to-slides). Does not print.
|
|
183
|
+
* @param {Object} opts - { videoCode, language, withTime, timeoutMs }
|
|
184
|
+
* @returns {Promise<string>} title + transcript text or throws
|
|
185
|
+
*/
|
|
186
|
+
export async function getYoutubeContent(opts) {
|
|
187
|
+
const apiKey = await getApiKey();
|
|
188
|
+
if (!apiKey) throw new Error(NO_KEY_MESSAGE.trim());
|
|
189
|
+
const raw = opts?.videoCode != null ? String(opts.videoCode).trim() : '';
|
|
190
|
+
if (!raw) throw new Error('YouTube video URL or video ID is required.');
|
|
191
|
+
const videoCode = extractVideoId(raw);
|
|
192
|
+
if (!videoCode) {
|
|
193
|
+
throw new Error('Invalid YouTube URL or video ID. Use a link or an 11-character video ID.');
|
|
194
|
+
}
|
|
195
|
+
const apiBase = await getApiBase();
|
|
196
|
+
const timeoutMs =
|
|
197
|
+
Number.isFinite(opts?.timeoutMs) && opts.timeoutMs > 0 ? opts.timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
198
|
+
const params = new URLSearchParams({ video_code: videoCode });
|
|
199
|
+
if (opts?.language && String(opts.language).trim()) params.set('language', String(opts.language).trim());
|
|
200
|
+
if (opts?.withTime) params.set('with_time', 'true');
|
|
201
|
+
const payload = await fetchSubtitling(apiBase, apiKey, params, timeoutMs);
|
|
202
|
+
const data = payload?.data ?? {};
|
|
203
|
+
const title = data?.title ?? '';
|
|
204
|
+
const contents = data?.contents ?? [];
|
|
205
|
+
const text = formatContents(contents, opts?.withTime ?? false);
|
|
206
|
+
if (!text || text.trim() === '') {
|
|
207
|
+
throw new Error(`No subtitles found for video ${videoCode}`);
|
|
208
|
+
}
|
|
209
|
+
if (title) return `# ${title}\n\n${text}`;
|
|
210
|
+
return text;
|
|
211
|
+
}
|