felo-ai 0.2.24 → 0.2.28
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 +11 -0
- package/felo-livedoc/SKILL.md +21 -0
- package/felo-livedoc/scripts/run_livedoc.mjs +86 -0
- package/felo-superAgent/SKILL.md +12 -18
- package/package.json +2 -2
- package/src/cli.js +55 -0
- package/src/livedoc.js +133 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ 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.25] - 2026-03-17
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`felo livedoc download <id> <resource_id>`**: download a resource's source file directly to disk; follows the 302 redirect to the S3 presigned URL and streams the file; supports `--output <path>` to specify the destination filename (defaults to the filename from the `Content-Disposition` header)
|
|
13
|
+
- **`felo livedoc content <id> <resource_id>`**: fetch the extracted text content of a resource; supported for document, web, video, ai_doc, ai_ppt, text, voice, and mindmap types
|
|
14
|
+
- **`felo livedoc ppt-retrieve <id>`**: deep content retrieval from a specific PPT slide page; requires `--resource-id`, `--page-number`, and `--query`; supports `--max-chunk` (default 3); output format is identical to `retrieve`
|
|
15
|
+
- **`add-urls` custom title support**: the API now accepts each URL entry as either a plain string or a `{"url": "...", "title": "..."}` object, allowing custom resource titles when adding URLs
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
8
19
|
## [0.2.18] - 2026-03-14
|
|
9
20
|
|
|
10
21
|
### Added
|
package/felo-livedoc/SKILL.md
CHANGED
|
@@ -88,6 +88,8 @@ node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs add-doc SHORT_ID --ti
|
|
|
88
88
|
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs add-urls SHORT_ID --urls "https://example.com,https://example.org"
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
+
Each URL can also be passed as a `url:title` pair using the `--url-titles` option, or by providing a JSON array. The API accepts both plain strings and `{"url": "...", "title": "..."}` objects — the script passes them through as-is when using `--json` mode. To add URLs with custom titles, use the JSON flag and construct the body manually, or rely on the auto-title from the page.
|
|
92
|
+
|
|
91
93
|
**Upload a file:**
|
|
92
94
|
```bash
|
|
93
95
|
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs upload SHORT_ID --file ./document.pdf
|
|
@@ -126,6 +128,25 @@ node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs retrieve SHORT_ID --q
|
|
|
126
128
|
```bash
|
|
127
129
|
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs retrieve SHORT_ID --query "your search query" --resource-ids "id1,id2,id3"
|
|
128
130
|
```
|
|
131
|
+
|
|
132
|
+
### Resource File & Content
|
|
133
|
+
|
|
134
|
+
**Download resource source file (get presigned URL):**
|
|
135
|
+
```bash
|
|
136
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs download SHORT_ID RESOURCE_ID
|
|
137
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs download SHORT_ID RESOURCE_ID --expires-in 7200
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Get extracted text content of a resource:**
|
|
141
|
+
```bash
|
|
142
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs content SHORT_ID RESOURCE_ID
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**PPT page deep retrieval:**
|
|
146
|
+
```bash
|
|
147
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs ppt-retrieve SHORT_ID --resource-id RESOURCE_ID --page-number 3 --query "pricing information"
|
|
148
|
+
node ~/.agents/skills/felo-livedoc/scripts/run_livedoc.mjs ppt-retrieve SHORT_ID --resource-id RESOURCE_ID --page-number 3 --query "pricing information" --max-chunk 5
|
|
149
|
+
```
|
|
129
150
|
### Options
|
|
130
151
|
|
|
131
152
|
All commands support:
|
|
@@ -145,6 +145,9 @@ function usage() {
|
|
|
145
145
|
' remove-resource <short_id> <resource_id> Delete a resource',
|
|
146
146
|
' retrieve <short_id> Semantic search (--query required, --resource-ids optional)',
|
|
147
147
|
' route <short_id> Route relevant resources by query (--query required)',
|
|
148
|
+
' download <short_id> <resource_id> Download source file to disk',
|
|
149
|
+
' content <short_id> <resource_id> Get text content of a resource',
|
|
150
|
+
' ppt-retrieve <short_id> PPT page deep retrieval (--resource-id, --page-number, --query required)',
|
|
148
151
|
'',
|
|
149
152
|
'Options:',
|
|
150
153
|
' --name <name> LiveDoc name',
|
|
@@ -162,6 +165,11 @@ function usage() {
|
|
|
162
165
|
' --query <text> Retrieval/route query',
|
|
163
166
|
' --resource-ids <ids> Comma-separated resource IDs to search within (retrieve)',
|
|
164
167
|
' --max-resources <n> Max resources to return (route)',
|
|
168
|
+
' --resource-id <id> PPT resource ID (ppt-retrieve)',
|
|
169
|
+
' --page-number <n> PPT page number, starts from 1 (ppt-retrieve)',
|
|
170
|
+
' --max-chunk <n> Max chunks to return (ppt-retrieve, default 3)',
|
|
171
|
+
' --expires-in <s> Presigned URL expiry in seconds (download, default 3600)',
|
|
172
|
+
' --output <path> Output file path (download, default: filename from response)',
|
|
165
173
|
' -j, --json Output raw JSON',
|
|
166
174
|
' -t, --timeout <ms> Timeout in ms (default: 60000)',
|
|
167
175
|
' --help Show this help',
|
|
@@ -172,6 +180,7 @@ function parseArgs(argv) {
|
|
|
172
180
|
action: '', positional: [], name: '', description: '', icon: '',
|
|
173
181
|
keyword: '', page: '', size: '', type: '', content: '', title: '',
|
|
174
182
|
urls: '', file: '', convert: false, query: '', resourceIds: '', maxResources: '',
|
|
183
|
+
resourceId: '', pageNumber: '', maxChunk: '', expiresIn: '',
|
|
175
184
|
json: false, timeoutMs: DEFAULT_TIMEOUT_MS, help: false,
|
|
176
185
|
};
|
|
177
186
|
const positional = [];
|
|
@@ -194,6 +203,10 @@ function parseArgs(argv) {
|
|
|
194
203
|
else if (a === '--query') out.query = argv[++i] || '';
|
|
195
204
|
else if (a === '--resource-ids') out.resourceIds = argv[++i] || '';
|
|
196
205
|
else if (a === '--max-resources') out.maxResources = argv[++i] || '';
|
|
206
|
+
else if (a === '--resource-id') out.resourceId = argv[++i] || '';
|
|
207
|
+
else if (a === '--page-number') out.pageNumber = argv[++i] || '';
|
|
208
|
+
else if (a === '--max-chunk') out.maxChunk = argv[++i] || '';
|
|
209
|
+
else if (a === '--expires-in') out.expiresIn = argv[++i] || '';
|
|
197
210
|
else if (a === '-t' || a === '--timeout') {
|
|
198
211
|
const n = parseInt(argv[++i] || '', 10);
|
|
199
212
|
if (Number.isFinite(n) && n > 0) out.timeoutMs = n;
|
|
@@ -403,6 +416,79 @@ async function main() {
|
|
|
403
416
|
code = 0;
|
|
404
417
|
break;
|
|
405
418
|
}
|
|
419
|
+
case 'download': {
|
|
420
|
+
if (!shortId) { console.error('ERROR: short_id is required'); break; }
|
|
421
|
+
if (!resourceId) { console.error('ERROR: resource_id is required'); break; }
|
|
422
|
+
spinnerId = startSpinner('Downloading resource');
|
|
423
|
+
const dlUrl = `${apiBase}/v2/livedocs/${shortId}/resources/${resourceId}/download${args.expiresIn ? `?expires_in=${args.expiresIn}` : ''}`;
|
|
424
|
+
const dlRes = await fetchWithRetry(dlUrl, { method: 'GET', headers: { Authorization: `Bearer ${apiKey}` }, redirect: 'follow' }, timeoutMs);
|
|
425
|
+
if (!dlRes.ok) {
|
|
426
|
+
let msg = dlRes.statusText;
|
|
427
|
+
try { const d = await dlRes.json(); msg = d?.message || d?.error || msg; } catch { /* ignore */ }
|
|
428
|
+
console.error(`ERROR: ${dlRes.status} ${msg}`);
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
let filename = args.output;
|
|
432
|
+
if (!filename) {
|
|
433
|
+
const cd = dlRes.headers.get('content-disposition') || '';
|
|
434
|
+
const match = cd.match(/filename\*?=(?:UTF-8'')?["']?([^"';\r\n]+)/i);
|
|
435
|
+
filename = match ? decodeURIComponent(match[1].trim()) : resourceId;
|
|
436
|
+
}
|
|
437
|
+
const { createWriteStream } = await import('fs');
|
|
438
|
+
const writer = createWriteStream(filename);
|
|
439
|
+
const reader = dlRes.body.getReader();
|
|
440
|
+
await new Promise((resolve, reject) => {
|
|
441
|
+
writer.on('error', reject);
|
|
442
|
+
writer.on('finish', resolve);
|
|
443
|
+
const pump = async () => {
|
|
444
|
+
try {
|
|
445
|
+
while (true) {
|
|
446
|
+
const { done, value } = await reader.read();
|
|
447
|
+
if (done) { writer.end(); break; }
|
|
448
|
+
writer.write(value);
|
|
449
|
+
}
|
|
450
|
+
} catch (err) { reject(err); }
|
|
451
|
+
};
|
|
452
|
+
pump();
|
|
453
|
+
});
|
|
454
|
+
process.stdout.write(`Downloaded: ${filename}\n`);
|
|
455
|
+
code = 0;
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
case 'content': {
|
|
459
|
+
if (!shortId) { console.error('ERROR: short_id is required'); break; }
|
|
460
|
+
if (!resourceId) { console.error('ERROR: resource_id is required'); break; }
|
|
461
|
+
spinnerId = startSpinner('Fetching resource content');
|
|
462
|
+
const payload = await apiRequest('GET', `/livedocs/${shortId}/resources/${resourceId}/content`, null, apiKey, apiBase, timeoutMs);
|
|
463
|
+
if (json) { console.log(JSON.stringify(payload, null, 2)); }
|
|
464
|
+
else {
|
|
465
|
+
const d = payload?.data;
|
|
466
|
+
process.stdout.write(`## ${d?.title || '(untitled)'}\n- Type: ${d?.type}\n\n${d?.content || '(empty)'}\n`);
|
|
467
|
+
}
|
|
468
|
+
code = 0;
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
case 'ppt-retrieve': {
|
|
472
|
+
if (!shortId) { console.error('ERROR: short_id is required'); break; }
|
|
473
|
+
if (!args.resourceId) { console.error('ERROR: --resource-id is required'); break; }
|
|
474
|
+
if (!args.pageNumber) { console.error('ERROR: --page-number is required'); break; }
|
|
475
|
+
if (!args.query) { console.error('ERROR: --query is required'); break; }
|
|
476
|
+
spinnerId = startSpinner('Retrieving PPT page content');
|
|
477
|
+
const body = { resource_id: args.resourceId, page_number: parseInt(args.pageNumber, 10), query: args.query };
|
|
478
|
+
if (args.maxChunk) { const n = parseInt(args.maxChunk, 10); if (Number.isFinite(n) && n > 0) body.max_chunk = n; }
|
|
479
|
+
const payload = await apiRequest('POST', `/livedocs/${shortId}/resources/ppt-retrieve`, body, apiKey, apiBase, timeoutMs);
|
|
480
|
+
if (json) { console.log(JSON.stringify(payload, null, 2)); }
|
|
481
|
+
else {
|
|
482
|
+
const results = payload?.data || [];
|
|
483
|
+
if (!results.length) { process.stderr.write('No results found.\n'); }
|
|
484
|
+
else {
|
|
485
|
+
process.stdout.write(`Found ${results.length} result(s)\n\n`);
|
|
486
|
+
for (const r of results) process.stdout.write(formatRetrieveResult(r));
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
code = 0;
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
406
492
|
default:
|
|
407
493
|
console.error(`Unknown action: ${action}`);
|
|
408
494
|
usage();
|
package/felo-superAgent/SKILL.md
CHANGED
|
@@ -230,8 +230,7 @@ Examples:
|
|
|
230
230
|
node felo-superAgent/scripts/run_superagent.mjs \
|
|
231
231
|
--query "USER_QUERY_HERE" \
|
|
232
232
|
--live-doc-id "LIVE_DOC_ID" \
|
|
233
|
-
--accept-language en
|
|
234
|
-
--timeout 3600
|
|
233
|
+
--accept-language en
|
|
235
234
|
```
|
|
236
235
|
|
|
237
236
|
**New conversation with skill ID (e.g., tweet writing):**
|
|
@@ -240,8 +239,7 @@ node felo-superAgent/scripts/run_superagent.mjs \
|
|
|
240
239
|
--query "Write a tweet about the latest AI trends" \
|
|
241
240
|
--live-doc-id "LIVE_DOC_ID" \
|
|
242
241
|
--skill-id twitter-writer \
|
|
243
|
-
--accept-language en
|
|
244
|
-
--timeout 3600
|
|
242
|
+
--accept-language en
|
|
245
243
|
```
|
|
246
244
|
|
|
247
245
|
**Follow-up question (DEFAULT for 2nd+ messages):**
|
|
@@ -249,8 +247,7 @@ node felo-superAgent/scripts/run_superagent.mjs \
|
|
|
249
247
|
node felo-superAgent/scripts/run_superagent.mjs \
|
|
250
248
|
--query "USER_FOLLOW_UP_QUERY" \
|
|
251
249
|
--thread-id "THREAD_SHORT_ID_FROM_PREVIOUS" \
|
|
252
|
-
--live-doc-id "LIVE_DOC_ID"
|
|
253
|
-
--timeout 3600
|
|
250
|
+
--live-doc-id "LIVE_DOC_ID"
|
|
254
251
|
```
|
|
255
252
|
|
|
256
253
|
### Step 6: Extract State from stderr (Do NOT Re-output the Answer)
|
|
@@ -284,7 +281,7 @@ User: "What is quantum computing?"
|
|
|
284
281
|
node felo-superAgent/scripts/run_superagent.mjs \
|
|
285
282
|
--query "What is quantum computing?" \
|
|
286
283
|
--live-doc-id "QPetunwpGnkKuZHStP7gwt" \
|
|
287
|
-
--accept-language en
|
|
284
|
+
--accept-language en
|
|
288
285
|
```
|
|
289
286
|
**Step 6:** The answer is already streamed to the user. Extract from stderr `[state]` line: `thread_short_id = "CmYpuGwBgCnrUdDx5ZtmxA"`, `live_doc_id = "QPetunwpGnkKuZHStP7gwt"`. Do NOT repeat the answer.
|
|
290
287
|
|
|
@@ -299,8 +296,7 @@ User: "What are its practical applications?"
|
|
|
299
296
|
node felo-superAgent/scripts/run_superagent.mjs \
|
|
300
297
|
--query "What are its practical applications?" \
|
|
301
298
|
--thread-id "CmYpuGwBgCnrUdDx5ZtmxA" \
|
|
302
|
-
--live-doc-id "QPetunwpGnkKuZHStP7gwt"
|
|
303
|
-
--timeout 3600
|
|
299
|
+
--live-doc-id "QPetunwpGnkKuZHStP7gwt"
|
|
304
300
|
```
|
|
305
301
|
**Step 6:** Answer already streamed. Extract updated `thread_short_id` from stderr `[state]` line (may be the same), keep `live_doc_id`.
|
|
306
302
|
|
|
@@ -326,7 +322,7 @@ node felo-superAgent/scripts/run_superagent.mjs \
|
|
|
326
322
|
--query "Help me write a tweet about AI trends" \
|
|
327
323
|
--live-doc-id "QPetunwpGnkKuZHStP7gwt" \
|
|
328
324
|
--skill-id twitter-writer \
|
|
329
|
-
--accept-language en
|
|
325
|
+
--accept-language en
|
|
330
326
|
```
|
|
331
327
|
**Step 6:** Answer already streamed. Extract new `thread_short_id` from stderr `[state]` line, keep same `live_doc_id`.
|
|
332
328
|
|
|
@@ -340,8 +336,7 @@ User: "Make it more casual and add some emojis"
|
|
|
340
336
|
node felo-superAgent/scripts/run_superagent.mjs \
|
|
341
337
|
--query "Make it more casual and add some emojis" \
|
|
342
338
|
--thread-id "NEW_THREAD_FROM_TWEET" \
|
|
343
|
-
--live-doc-id "QPetunwpGnkKuZHStP7gwt"
|
|
344
|
-
--timeout 3600
|
|
339
|
+
--live-doc-id "QPetunwpGnkKuZHStP7gwt"
|
|
345
340
|
```
|
|
346
341
|
|
|
347
342
|
### Example C: Logo Design
|
|
@@ -357,7 +352,7 @@ node felo-superAgent/scripts/run_superagent.mjs \
|
|
|
357
352
|
--query "Design a logo for my coffee shop called Bean & Brew" \
|
|
358
353
|
--live-doc-id "QPetunwpGnkKuZHStP7gwt" \
|
|
359
354
|
--skill-id logo-and-branding \
|
|
360
|
-
--accept-language en
|
|
355
|
+
--accept-language en
|
|
361
356
|
```
|
|
362
357
|
|
|
363
358
|
### Example D: E-commerce Product Image
|
|
@@ -373,7 +368,7 @@ node felo-superAgent/scripts/run_superagent.mjs \
|
|
|
373
368
|
--query "Generate a product image for a wireless headphone on white background" \
|
|
374
369
|
--live-doc-id "QPetunwpGnkKuZHStP7gwt" \
|
|
375
370
|
--skill-id ecommerce-product-image \
|
|
376
|
-
--accept-language en
|
|
371
|
+
--accept-language en
|
|
377
372
|
```
|
|
378
373
|
|
|
379
374
|
### Example E: User Requests a New Canvas
|
|
@@ -394,7 +389,6 @@ Extract new `live_doc_id`. Discard the old one. All subsequent calls use the new
|
|
|
394
389
|
- `--query <text>` (REQUIRED) — User question, 1-2000 characters
|
|
395
390
|
- `--live-doc-id <id>` (REQUIRED for new conversations) — LiveDoc ID (`live_doc_id`) to associate with
|
|
396
391
|
- `--thread-id <id>` — Thread ID from previous response, for follow-up conversations
|
|
397
|
-
- `--timeout <seconds>` — Request/stream timeout, default 3600 seconds
|
|
398
392
|
|
|
399
393
|
**Skill parameters (new conversations only, ignored in follow-up):**
|
|
400
394
|
- `--skill-id <id>` — Skill ID (see Constraint #8 for available skill IDs)
|
|
@@ -505,9 +499,8 @@ To use this skill, you need to set up your Felo API Key:
|
|
|
505
499
|
|
|
506
500
|
### Timeout Handling
|
|
507
501
|
|
|
508
|
-
-
|
|
509
|
-
-
|
|
510
|
-
- **Bash tool timeout:** MUST be set to at least 600000ms (10 minutes) when executing the script
|
|
502
|
+
- The SSE stream has its own idle timeout: 2 hours (no data received). The stream stays open as long as data keeps flowing.
|
|
503
|
+
- **Bash tool timeout:** MUST be set to at least 600000ms (10 minutes) when executing the script, because the SSE stream can run for a long time.
|
|
511
504
|
|
|
512
505
|
## Important Notes
|
|
513
506
|
|
|
@@ -561,3 +554,4 @@ Do NOT repeat or summarize the answer (already shown)
|
|
|
561
554
|
- [SuperAgent API (Felo Open Platform)](https://openapi.felo.ai/docs/api-reference/v2/superagent.html)
|
|
562
555
|
- [Felo Open Platform](https://openapi.felo.ai/docs/)
|
|
563
556
|
- [Get API Key](https://felo.ai) (Settings -> API Keys)
|
|
557
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "felo-ai",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.28",
|
|
4
4
|
"description": "Felo AI CLI - real-time search, PPT generation, SuperAgent conversation, LiveDoc management, web fetch, YouTube subtitles, LiveDoc knowledge base, and X (Twitter) search from the terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/cli.js",
|
|
@@ -36,6 +36,6 @@
|
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"test": "node --test tests/",
|
|
39
|
-
"publish": "npm publish"
|
|
39
|
+
"publish:local": "npm publish"
|
|
40
40
|
}
|
|
41
41
|
}
|
package/src/cli.js
CHANGED
|
@@ -674,6 +674,61 @@ livedocCmd
|
|
|
674
674
|
flushStdioThenExit(code);
|
|
675
675
|
});
|
|
676
676
|
|
|
677
|
+
livedocCmd
|
|
678
|
+
.command("download <short_id> <resource_id>")
|
|
679
|
+
.description("Download a resource source file to disk")
|
|
680
|
+
.option("--output <path>", "output file path (default: filename from response)")
|
|
681
|
+
.option("--expires-in <seconds>", "presigned URL expiry in seconds (default 3600)")
|
|
682
|
+
.option("-t, --timeout <seconds>", "request timeout in seconds", "60")
|
|
683
|
+
.action(async (shortId, resourceId, opts) => {
|
|
684
|
+
const timeoutMs = parseInt(opts.timeout, 10) * 1000;
|
|
685
|
+
const code = await livedoc.downloadResource(shortId, resourceId, {
|
|
686
|
+
output: opts.output,
|
|
687
|
+
expiresIn: opts.expiresIn,
|
|
688
|
+
timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
|
|
689
|
+
});
|
|
690
|
+
process.exitCode = code;
|
|
691
|
+
flushStdioThenExit(code);
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
livedocCmd
|
|
695
|
+
.command("content <short_id> <resource_id>")
|
|
696
|
+
.description("Get extracted text content of a resource")
|
|
697
|
+
.option("-j, --json", "output raw JSON")
|
|
698
|
+
.option("-t, --timeout <seconds>", "request timeout in seconds", "60")
|
|
699
|
+
.action(async (shortId, resourceId, opts) => {
|
|
700
|
+
const timeoutMs = parseInt(opts.timeout, 10) * 1000;
|
|
701
|
+
const code = await livedoc.getResourceContent(shortId, resourceId, {
|
|
702
|
+
json: opts.json,
|
|
703
|
+
timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
|
|
704
|
+
});
|
|
705
|
+
process.exitCode = code;
|
|
706
|
+
flushStdioThenExit(code);
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
livedocCmd
|
|
710
|
+
.command("ppt-retrieve <short_id>")
|
|
711
|
+
.description("Deep content retrieval from a specific PPT page")
|
|
712
|
+
.requiredOption("--resource-id <id>", "PPT resource ID")
|
|
713
|
+
.requiredOption("--page-number <n>", "page number (starts from 1)")
|
|
714
|
+
.requiredOption("--query <query>", "retrieval query")
|
|
715
|
+
.option("--max-chunk <n>", "max chunks to return (default 3)")
|
|
716
|
+
.option("-j, --json", "output raw JSON")
|
|
717
|
+
.option("-t, --timeout <seconds>", "request timeout in seconds", "60")
|
|
718
|
+
.action(async (shortId, opts) => {
|
|
719
|
+
const timeoutMs = parseInt(opts.timeout, 10) * 1000;
|
|
720
|
+
const code = await livedoc.pptRetrieve(shortId, {
|
|
721
|
+
resourceId: opts.resourceId,
|
|
722
|
+
pageNumber: opts.pageNumber,
|
|
723
|
+
query: opts.query,
|
|
724
|
+
maxChunk: opts.maxChunk,
|
|
725
|
+
json: opts.json,
|
|
726
|
+
timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
|
|
727
|
+
});
|
|
728
|
+
process.exitCode = code;
|
|
729
|
+
flushStdioThenExit(code);
|
|
730
|
+
});
|
|
731
|
+
|
|
677
732
|
program
|
|
678
733
|
.command("summarize")
|
|
679
734
|
.description("Summarize text or URL (coming when API is available)")
|
package/src/livedoc.js
CHANGED
|
@@ -432,3 +432,136 @@ export async function retrieve(shortId, opts = {}) {
|
|
|
432
432
|
return 1;
|
|
433
433
|
} finally { stopSpinner(spinnerId); }
|
|
434
434
|
}
|
|
435
|
+
|
|
436
|
+
export async function downloadResource(shortId, resourceId, opts = {}) {
|
|
437
|
+
const apiKey = await getApiKey();
|
|
438
|
+
if (!apiKey) { console.error(NO_KEY_MESSAGE.trim()); return 1; }
|
|
439
|
+
if (!shortId) { process.stderr.write('ERROR: short_id is required.\n'); return 1; }
|
|
440
|
+
if (!resourceId) { process.stderr.write('ERROR: resource_id is required.\n'); return 1; }
|
|
441
|
+
|
|
442
|
+
const apiBase = await getApiBase();
|
|
443
|
+
const timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
444
|
+
const spinnerId = startSpinner('Downloading resource');
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
const params = new URLSearchParams();
|
|
448
|
+
if (opts.expiresIn) params.set('expires_in', opts.expiresIn);
|
|
449
|
+
const qs = params.toString();
|
|
450
|
+
const url = `${apiBase}/v2/livedocs/${shortId}/resources/${resourceId}/download${qs ? `?${qs}` : ''}`;
|
|
451
|
+
|
|
452
|
+
// Follow redirects to get the actual file stream from S3
|
|
453
|
+
const res = await fetchWithTimeoutAndRetry(
|
|
454
|
+
url,
|
|
455
|
+
{ method: 'GET', headers: { Authorization: `Bearer ${apiKey}` }, redirect: 'follow' },
|
|
456
|
+
timeoutMs,
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
if (!res.ok) {
|
|
460
|
+
let msg = res.statusText;
|
|
461
|
+
try { const d = await res.json(); msg = getMessage(d) || msg; } catch { /* ignore */ }
|
|
462
|
+
process.stderr.write(`ERROR: ${res.status} ${msg}\n`);
|
|
463
|
+
return 1;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Determine output filename
|
|
467
|
+
let filename = opts.output;
|
|
468
|
+
if (!filename) {
|
|
469
|
+
// Try Content-Disposition header first
|
|
470
|
+
const cd = res.headers.get('content-disposition') || '';
|
|
471
|
+
const match = cd.match(/filename\*?=(?:UTF-8'')?["']?([^"';\r\n]+)/i);
|
|
472
|
+
if (match) {
|
|
473
|
+
filename = decodeURIComponent(match[1].trim());
|
|
474
|
+
} else {
|
|
475
|
+
// Fall back to resource_id as filename
|
|
476
|
+
filename = resourceId;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Write file stream to disk
|
|
481
|
+
const { createWriteStream } = await import('fs');
|
|
482
|
+
const writer = createWriteStream(filename);
|
|
483
|
+
const reader = res.body.getReader();
|
|
484
|
+
await new Promise((resolve, reject) => {
|
|
485
|
+
writer.on('error', reject);
|
|
486
|
+
writer.on('finish', resolve);
|
|
487
|
+
const pump = async () => {
|
|
488
|
+
try {
|
|
489
|
+
while (true) {
|
|
490
|
+
const { done, value } = await reader.read();
|
|
491
|
+
if (done) { writer.end(); break; }
|
|
492
|
+
writer.write(value);
|
|
493
|
+
}
|
|
494
|
+
} catch (err) { reject(err); }
|
|
495
|
+
};
|
|
496
|
+
pump();
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
stopSpinner(spinnerId);
|
|
500
|
+
process.stdout.write(`Downloaded: ${filename}\n`);
|
|
501
|
+
return 0;
|
|
502
|
+
} catch (err) {
|
|
503
|
+
process.stderr.write(`Failed to download resource: ${err?.message || err}\n`);
|
|
504
|
+
return 1;
|
|
505
|
+
} finally { stopSpinner(spinnerId); }
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export async function getResourceContent(shortId, resourceId, opts = {}) {
|
|
509
|
+
const apiKey = await getApiKey();
|
|
510
|
+
if (!apiKey) { console.error(NO_KEY_MESSAGE.trim()); return 1; }
|
|
511
|
+
if (!shortId) { process.stderr.write('ERROR: short_id is required.\n'); return 1; }
|
|
512
|
+
if (!resourceId) { process.stderr.write('ERROR: resource_id is required.\n'); return 1; }
|
|
513
|
+
|
|
514
|
+
const apiBase = await getApiBase();
|
|
515
|
+
const timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
516
|
+
const spinnerId = startSpinner('Fetching resource content');
|
|
517
|
+
|
|
518
|
+
try {
|
|
519
|
+
const payload = await apiRequest('GET', `/livedocs/${shortId}/resources/${resourceId}/content`, null, apiKey, apiBase, timeoutMs);
|
|
520
|
+
if (opts.json) { console.log(JSON.stringify(payload, null, 2)); return 0; }
|
|
521
|
+
const d = payload?.data;
|
|
522
|
+
if (!d) { process.stderr.write('No content returned.\n'); return 0; }
|
|
523
|
+
process.stdout.write(`## ${d.title || '(untitled)'}\n`);
|
|
524
|
+
process.stdout.write(`- Type: ${d.type}\n\n`);
|
|
525
|
+
process.stdout.write(d.content || '(empty)');
|
|
526
|
+
process.stdout.write('\n');
|
|
527
|
+
return 0;
|
|
528
|
+
} catch (err) {
|
|
529
|
+
process.stderr.write(`Failed to get resource content: ${err?.message || err}\n`);
|
|
530
|
+
return 1;
|
|
531
|
+
} finally { stopSpinner(spinnerId); }
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export async function pptRetrieve(shortId, opts = {}) {
|
|
535
|
+
const apiKey = await getApiKey();
|
|
536
|
+
if (!apiKey) { console.error(NO_KEY_MESSAGE.trim()); return 1; }
|
|
537
|
+
if (!shortId) { process.stderr.write('ERROR: short_id is required.\n'); return 1; }
|
|
538
|
+
if (!opts.resourceId) { process.stderr.write('ERROR: --resource-id is required.\n'); return 1; }
|
|
539
|
+
if (!opts.pageNumber) { process.stderr.write('ERROR: --page-number is required.\n'); return 1; }
|
|
540
|
+
if (!opts.query) { process.stderr.write('ERROR: --query is required.\n'); return 1; }
|
|
541
|
+
|
|
542
|
+
const apiBase = await getApiBase();
|
|
543
|
+
const timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
544
|
+
const spinnerId = startSpinner('Retrieving PPT page content');
|
|
545
|
+
|
|
546
|
+
try {
|
|
547
|
+
const body = {
|
|
548
|
+
resource_id: opts.resourceId,
|
|
549
|
+
page_number: parseInt(opts.pageNumber, 10),
|
|
550
|
+
query: opts.query,
|
|
551
|
+
};
|
|
552
|
+
if (opts.maxChunk) {
|
|
553
|
+
const n = parseInt(opts.maxChunk, 10);
|
|
554
|
+
if (Number.isFinite(n) && n > 0) body.max_chunk = n;
|
|
555
|
+
}
|
|
556
|
+
const payload = await apiRequest('POST', `/livedocs/${shortId}/resources/ppt-retrieve`, body, apiKey, apiBase, timeoutMs);
|
|
557
|
+
if (opts.json) { console.log(JSON.stringify(payload, null, 2)); return 0; }
|
|
558
|
+
const results = payload?.data || [];
|
|
559
|
+
if (!results.length) { process.stderr.write('No results found.\n'); return 0; }
|
|
560
|
+
process.stdout.write(`Found ${results.length} result(s)\n\n`);
|
|
561
|
+
for (const r of results) { process.stdout.write(formatRetrieveResult(r)); }
|
|
562
|
+
return 0;
|
|
563
|
+
} catch (err) {
|
|
564
|
+
process.stderr.write(`Failed to ppt-retrieve: ${err?.message || err}\n`);
|
|
565
|
+
return 1;
|
|
566
|
+
} finally { stopSpinner(spinnerId); }
|
|
567
|
+
}
|