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 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
@@ -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();
@@ -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 --timeout 3600
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 --timeout 3600
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 --timeout 3600
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 --timeout 3600
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
- - Default timeout: 3600 seconds (recommended for all SuperAgent calls due to SSE streaming)
509
- - Idle timeout: 2 hours (no data received)
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.24",
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
+ }