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 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.12] - 2026-03-102
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 payload = await apiRequest('POST', `/livedocs/${shortId}/resources/retrieve`, { content: args.query }, apiKey, apiBase, timeoutMs);
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 对话与流式输出,支持 LiveDoc 与连续会话。**
3
+ **AI 对话与流式输出,支持连续会话。**
4
4
 
5
- 通过 Felo Open Platform 的 SuperAgent API,在 Claude Code 中发起与 SuperAgent 的对话、接收 SSE 流式回复,并可关联 LiveDoc、查询会话详情与资源。
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
- - **连续对话**:通过 `live_doc_short_id` 在已有 LiveDoc 上继续提问
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
- | `--query <text>` | 用户问题(必填,1–2000 字符) |
90
- | `--live-doc-id <id>` | 复用已有 LiveDoc short_id(连续对话) |
91
- | `--accept-language <lang>` | 语言偏好,如 zh / en |
92
- | `--timeout <seconds>` | 请求/流超时,默认 60 |
93
- | `--json` | 输出 JSON(含 answer、thread_short_id、live_doc_short_id) |
94
- | `--verbose` | 将流连接信息打到 stderr |
95
- | `--help` | 显示帮助 |
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` 调用「查询会话详情」接口,用 `live_doc_short_id` 调用「列举 LiveDoc 资源」等接口。
162
+ 可用 `thread_short_id` 调用「查询会话详情」接口,`live_doc_short_id` 可传入 `felo-livedoc` 查询相关资源。
119
163
 
120
164
  ---
121
165
 
@@ -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 集成**:希望回答与 LiveDoc 关联、可追溯资源
14
- - **连续对话**:在已有会话/文档上继续提问(传入 `live_doc_short_id`)
15
- - **多轮对话**:需要 thread_short_id / live_doc_short_id 以便后续查询会话详情或 LiveDoc 资源
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.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.14",
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
- /** 流式读取空闲超时:连续这么久未收到任何数据则断开,默认 5 分钟(生图等长任务需较久) */
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(`已收到: ${data.query}`);
354
- } else if (type === 'processing' && onStatusMessage && data?.message) {
355
- onStatusMessage(data.message);
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 = (process.env.FELO_API_BASE?.trim() || DEFAULT_API_BASE).replace(/\/$/, '');
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 || '图片'}](${liveDocUrl})`);
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 || '文档'}](${liveDocUrl})`);
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
- '(流式无客户端超时;若内部接口能拿到完整流,多为代理/防火墙在等待生图等长任务时空闲断连,可直连或调大代理空闲超时后重试)\n'
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
+ }
@@ -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
+ }