felo-ai 0.2.24 → 0.2.25

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,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.24] - 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
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "felo-ai",
3
- "version": "0.2.24",
3
+ "version": "0.2.25",
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
+ }