felo-ai 0.2.28 → 0.2.31
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/.claude-plugin/marketplace.json +85 -0
- package/CHANGELOG.md +8 -0
- package/felo-content-to-slides/SKILL.md +4 -0
- package/felo-livedoc/README.md +13 -0
- package/felo-livedoc/SKILL.md +81 -2
- package/felo-livedoc/scripts/run_livedoc.mjs +195 -4
- package/felo-slides/SKILL.md +21 -0
- package/felo-slides/scripts/run_ppt_task.mjs +34 -8
- package/felo-web-fetch/scripts/run_web_fetch.mjs +2 -1
- package/felo-x-search/scripts/run_x_search.mjs +2 -1
- package/felo-youtube-subtitling/scripts/run_youtube_subtitling.mjs +2 -1
- package/package.json +1 -1
- package/src/cli.js +255 -3
- package/src/contentToSlides.js +1 -0
- package/src/livedoc.js +289 -1
- package/src/slides.js +109 -12
- package/src/webFetch.js +2 -1
- package/src/xSearch.js +2 -1
- package/src/youtubeSubtitling.js +2 -1
package/src/livedoc.js
CHANGED
|
@@ -12,6 +12,7 @@ const STATUS_PAD = 56;
|
|
|
12
12
|
// ── Shared helpers ──
|
|
13
13
|
|
|
14
14
|
function startSpinner(message) {
|
|
15
|
+
if (!process.stderr.isTTY) return null;
|
|
15
16
|
const start = Date.now();
|
|
16
17
|
let i = 0;
|
|
17
18
|
const id = setInterval(() => {
|
|
@@ -25,7 +26,7 @@ function startSpinner(message) {
|
|
|
25
26
|
|
|
26
27
|
function stopSpinner(id) {
|
|
27
28
|
if (id != null) clearInterval(id);
|
|
28
|
-
process.stderr.write(`\r${' '.repeat(STATUS_PAD)}\r`);
|
|
29
|
+
if (process.stderr.isTTY) process.stderr.write(`\r${' '.repeat(STATUS_PAD)}\r`);
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
async function getApiBase() {
|
|
@@ -374,6 +375,31 @@ export async function removeResource(shortId, resourceId, opts = {}) {
|
|
|
374
375
|
} finally { stopSpinner(spinnerId); }
|
|
375
376
|
}
|
|
376
377
|
|
|
378
|
+
export async function updateResource(shortId, resourceId, opts = {}) {
|
|
379
|
+
const apiKey = await getApiKey();
|
|
380
|
+
if (!apiKey) { console.error(NO_KEY_MESSAGE.trim()); return 1; }
|
|
381
|
+
if (!shortId || !resourceId) { process.stderr.write('ERROR: short_id and resource_id are required.\n'); return 1; }
|
|
382
|
+
|
|
383
|
+
const apiBase = await getApiBase();
|
|
384
|
+
const timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
385
|
+
const spinnerId = startSpinner('Updating resource');
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
const body = {};
|
|
389
|
+
if (opts.title !== undefined) body.title = opts.title;
|
|
390
|
+
if (opts.snippet !== undefined) body.snippet = opts.snippet;
|
|
391
|
+
if (opts.thumbnail !== undefined) body.thumbnail = opts.thumbnail;
|
|
392
|
+
const payload = await apiRequest('PUT', `/livedocs/${shortId}/resources/${resourceId}`, body, apiKey, apiBase, timeoutMs);
|
|
393
|
+
if (opts.json) { console.log(JSON.stringify(payload, null, 2)); return 0; }
|
|
394
|
+
process.stdout.write('Resource updated!\n\n');
|
|
395
|
+
process.stdout.write(formatResource(payload?.data));
|
|
396
|
+
return 0;
|
|
397
|
+
} catch (err) {
|
|
398
|
+
process.stderr.write(`Failed to update resource: ${err?.message || err}\n`);
|
|
399
|
+
return 1;
|
|
400
|
+
} finally { stopSpinner(spinnerId); }
|
|
401
|
+
}
|
|
402
|
+
|
|
377
403
|
export async function route(shortId, opts = {}) {
|
|
378
404
|
const apiKey = await getApiKey();
|
|
379
405
|
if (!apiKey) { console.error(NO_KEY_MESSAGE.trim()); return 1; }
|
|
@@ -531,6 +557,268 @@ export async function getResourceContent(shortId, resourceId, opts = {}) {
|
|
|
531
557
|
} finally { stopSpinner(spinnerId); }
|
|
532
558
|
}
|
|
533
559
|
|
|
560
|
+
// ── README ──
|
|
561
|
+
|
|
562
|
+
export async function getReadme(shortId, opts = {}) {
|
|
563
|
+
const apiKey = await getApiKey();
|
|
564
|
+
if (!apiKey) { console.error(NO_KEY_MESSAGE.trim()); return 1; }
|
|
565
|
+
if (!shortId) { process.stderr.write('ERROR: short_id is required.\n'); return 1; }
|
|
566
|
+
|
|
567
|
+
const apiBase = await getApiBase();
|
|
568
|
+
const timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
569
|
+
const spinnerId = startSpinner('Fetching README');
|
|
570
|
+
|
|
571
|
+
try {
|
|
572
|
+
const payload = await apiRequest('GET', `/livedocs/${shortId}/readme`, null, apiKey, apiBase, timeoutMs);
|
|
573
|
+
if (opts.json) { console.log(JSON.stringify(payload, null, 2)); return 0; }
|
|
574
|
+
const content = payload?.data?.content ?? '';
|
|
575
|
+
process.stdout.write(content || '(empty)\n');
|
|
576
|
+
return 0;
|
|
577
|
+
} catch (err) {
|
|
578
|
+
process.stderr.write(`Failed to get README: ${err?.message || err}\n`);
|
|
579
|
+
return 1;
|
|
580
|
+
} finally { stopSpinner(spinnerId); }
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
export async function upsertReadme(shortId, opts = {}) {
|
|
584
|
+
const apiKey = await getApiKey();
|
|
585
|
+
if (!apiKey) { console.error(NO_KEY_MESSAGE.trim()); return 1; }
|
|
586
|
+
if (!shortId) { process.stderr.write('ERROR: short_id is required.\n'); return 1; }
|
|
587
|
+
if (opts.content === undefined || opts.content === null) { process.stderr.write('ERROR: --content is required.\n'); return 1; }
|
|
588
|
+
|
|
589
|
+
const apiBase = await getApiBase();
|
|
590
|
+
const timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
591
|
+
const spinnerId = startSpinner('Updating README');
|
|
592
|
+
|
|
593
|
+
try {
|
|
594
|
+
const payload = await apiRequest('PUT', `/livedocs/${shortId}/readme`, { content: opts.content }, apiKey, apiBase, timeoutMs);
|
|
595
|
+
if (opts.json) { console.log(JSON.stringify(payload, null, 2)); return 0; }
|
|
596
|
+
process.stdout.write('README updated.\n');
|
|
597
|
+
return 0;
|
|
598
|
+
} catch (err) {
|
|
599
|
+
process.stderr.write(`Failed to update README: ${err?.message || err}\n`);
|
|
600
|
+
return 1;
|
|
601
|
+
} finally { stopSpinner(spinnerId); }
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
export async function appendReadme(shortId, opts = {}) {
|
|
605
|
+
const apiKey = await getApiKey();
|
|
606
|
+
if (!apiKey) { console.error(NO_KEY_MESSAGE.trim()); return 1; }
|
|
607
|
+
if (!shortId) { process.stderr.write('ERROR: short_id is required.\n'); return 1; }
|
|
608
|
+
if (!opts.content) { process.stderr.write('ERROR: --content is required.\n'); return 1; }
|
|
609
|
+
|
|
610
|
+
const apiBase = await getApiBase();
|
|
611
|
+
const timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
612
|
+
const spinnerId = startSpinner('Appending to README');
|
|
613
|
+
|
|
614
|
+
try {
|
|
615
|
+
const payload = await apiRequest('POST', `/livedocs/${shortId}/readme/append`, { content: opts.content }, apiKey, apiBase, timeoutMs);
|
|
616
|
+
if (opts.json) { console.log(JSON.stringify(payload, null, 2)); return 0; }
|
|
617
|
+
process.stdout.write('README appended.\n');
|
|
618
|
+
return 0;
|
|
619
|
+
} catch (err) {
|
|
620
|
+
process.stderr.write(`Failed to append README: ${err?.message || err}\n`);
|
|
621
|
+
return 1;
|
|
622
|
+
} finally { stopSpinner(spinnerId); }
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
export async function deleteReadme(shortId, opts = {}) {
|
|
626
|
+
const apiKey = await getApiKey();
|
|
627
|
+
if (!apiKey) { console.error(NO_KEY_MESSAGE.trim()); return 1; }
|
|
628
|
+
if (!shortId) { process.stderr.write('ERROR: short_id is required.\n'); return 1; }
|
|
629
|
+
|
|
630
|
+
const apiBase = await getApiBase();
|
|
631
|
+
const timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
632
|
+
const spinnerId = startSpinner('Deleting README');
|
|
633
|
+
|
|
634
|
+
try {
|
|
635
|
+
await apiRequest('DELETE', `/livedocs/${shortId}/readme`, null, apiKey, apiBase, timeoutMs);
|
|
636
|
+
if (opts.json) { console.log(JSON.stringify({ status: 'ok' }, null, 2)); return 0; }
|
|
637
|
+
process.stdout.write('README deleted.\n');
|
|
638
|
+
return 0;
|
|
639
|
+
} catch (err) {
|
|
640
|
+
process.stderr.write(`Failed to delete README: ${err?.message || err}\n`);
|
|
641
|
+
return 1;
|
|
642
|
+
} finally { stopSpinner(spinnerId); }
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// ── Tasks ──
|
|
646
|
+
|
|
647
|
+
function formatTask(t) {
|
|
648
|
+
if (!t) return '';
|
|
649
|
+
let out = `### ${t.title || '(untitled)'}\n`;
|
|
650
|
+
out += `- Task ID: \`${t.id}\`\n`;
|
|
651
|
+
out += `- Status: ${t.status === 0 ? 'TODO' : t.status === 1 ? 'IN_PROGRESS' : t.status === 2 ? 'DONE' : t.status}\n`;
|
|
652
|
+
if (t.sort != null) out += `- Sort: ${t.sort}\n`;
|
|
653
|
+
if (t.description) out += `- Description: ${t.description}\n`;
|
|
654
|
+
if (t.labels?.length) out += `- Labels: ${t.labels.join(', ')}\n`;
|
|
655
|
+
if (t.created_at) out += `- Created: ${t.created_at}\n`;
|
|
656
|
+
out += '\n';
|
|
657
|
+
return out;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function formatTaskRecord(r) {
|
|
661
|
+
if (!r) return '';
|
|
662
|
+
let out = `- [${r.record_type}] `;
|
|
663
|
+
if (r.content) out += r.content;
|
|
664
|
+
else if (r.meta) out += JSON.stringify(r.meta);
|
|
665
|
+
out += ` (id: ${r.id}, ${r.created_at || ''})\n`;
|
|
666
|
+
return out;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
export async function listTasks(shortId, opts = {}) {
|
|
670
|
+
const apiKey = await getApiKey();
|
|
671
|
+
if (!apiKey) { console.error(NO_KEY_MESSAGE.trim()); return 1; }
|
|
672
|
+
if (!shortId) { process.stderr.write('ERROR: short_id is required.\n'); return 1; }
|
|
673
|
+
|
|
674
|
+
const apiBase = await getApiBase();
|
|
675
|
+
const timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
676
|
+
const spinnerId = startSpinner('Listing tasks');
|
|
677
|
+
|
|
678
|
+
try {
|
|
679
|
+
const params = new URLSearchParams();
|
|
680
|
+
if (opts.status !== undefined && opts.status !== '') params.set('status', opts.status);
|
|
681
|
+
if (opts.labels) opts.labels.split(',').map(l => l.trim()).filter(Boolean).forEach(l => params.append('labels', l));
|
|
682
|
+
if (opts.page) params.set('page', opts.page);
|
|
683
|
+
if (opts.size) params.set('size', opts.size);
|
|
684
|
+
const qs = params.toString();
|
|
685
|
+
const payload = await apiRequest('GET', `/livedocs/${shortId}/tasks${qs ? `?${qs}` : ''}`, null, apiKey, apiBase, timeoutMs);
|
|
686
|
+
if (opts.json) { console.log(JSON.stringify(payload, null, 2)); return 0; }
|
|
687
|
+
const items = payload?.data?.items || [];
|
|
688
|
+
if (!items.length) { process.stderr.write('No tasks found.\n'); return 0; }
|
|
689
|
+
process.stdout.write(`Found ${payload.data.total || items.length} task(s)\n\n`);
|
|
690
|
+
for (const t of items) process.stdout.write(formatTask(t));
|
|
691
|
+
return 0;
|
|
692
|
+
} catch (err) {
|
|
693
|
+
process.stderr.write(`Failed to list tasks: ${err?.message || err}\n`);
|
|
694
|
+
return 1;
|
|
695
|
+
} finally { stopSpinner(spinnerId); }
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
export async function createTask(shortId, opts = {}) {
|
|
699
|
+
const apiKey = await getApiKey();
|
|
700
|
+
if (!apiKey) { console.error(NO_KEY_MESSAGE.trim()); return 1; }
|
|
701
|
+
if (!shortId) { process.stderr.write('ERROR: short_id is required.\n'); return 1; }
|
|
702
|
+
if (!opts.title) { process.stderr.write('ERROR: --title is required.\n'); return 1; }
|
|
703
|
+
if (opts.status === undefined || opts.status === '') { process.stderr.write('ERROR: --status is required.\n'); return 1; }
|
|
704
|
+
if (opts.sort === undefined || opts.sort === '') { process.stderr.write('ERROR: --sort is required.\n'); return 1; }
|
|
705
|
+
|
|
706
|
+
const apiBase = await getApiBase();
|
|
707
|
+
const timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
708
|
+
const spinnerId = startSpinner('Creating task');
|
|
709
|
+
|
|
710
|
+
try {
|
|
711
|
+
const body = { title: opts.title, status: parseInt(opts.status, 10), sort: parseInt(opts.sort, 10) };
|
|
712
|
+
if (opts.description) body.description = opts.description;
|
|
713
|
+
if (opts.labels) body.labels = opts.labels.split(',').map(l => l.trim()).filter(Boolean);
|
|
714
|
+
const payload = await apiRequest('POST', `/livedocs/${shortId}/tasks`, body, apiKey, apiBase, timeoutMs);
|
|
715
|
+
if (opts.json) { console.log(JSON.stringify(payload, null, 2)); return 0; }
|
|
716
|
+
process.stdout.write('Task created!\n\n');
|
|
717
|
+
process.stdout.write(formatTask(payload?.data));
|
|
718
|
+
return 0;
|
|
719
|
+
} catch (err) {
|
|
720
|
+
process.stderr.write(`Failed to create task: ${err?.message || err}\n`);
|
|
721
|
+
return 1;
|
|
722
|
+
} finally { stopSpinner(spinnerId); }
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
export async function updateTask(shortId, taskId, opts = {}) {
|
|
726
|
+
const apiKey = await getApiKey();
|
|
727
|
+
if (!apiKey) { console.error(NO_KEY_MESSAGE.trim()); return 1; }
|
|
728
|
+
if (!shortId || !taskId) { process.stderr.write('ERROR: short_id and task_id are required.\n'); return 1; }
|
|
729
|
+
|
|
730
|
+
const apiBase = await getApiBase();
|
|
731
|
+
const timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
732
|
+
const spinnerId = startSpinner('Updating task');
|
|
733
|
+
|
|
734
|
+
try {
|
|
735
|
+
const body = {};
|
|
736
|
+
if (opts.title) body.title = opts.title;
|
|
737
|
+
if (opts.description !== undefined) body.description = opts.description;
|
|
738
|
+
if (opts.status !== undefined && opts.status !== '') body.status = parseInt(opts.status, 10);
|
|
739
|
+
if (opts.sort !== undefined && opts.sort !== '') body.sort = parseInt(opts.sort, 10);
|
|
740
|
+
if (opts.labels !== undefined) body.labels = opts.labels.split(',').map(l => l.trim()).filter(Boolean);
|
|
741
|
+
const payload = await apiRequest('PATCH', `/livedocs/${shortId}/tasks/${taskId}`, body, apiKey, apiBase, timeoutMs);
|
|
742
|
+
if (opts.json) { console.log(JSON.stringify(payload, null, 2)); return 0; }
|
|
743
|
+
process.stdout.write('Task updated!\n\n');
|
|
744
|
+
process.stdout.write(formatTask(payload?.data));
|
|
745
|
+
return 0;
|
|
746
|
+
} catch (err) {
|
|
747
|
+
process.stderr.write(`Failed to update task: ${err?.message || err}\n`);
|
|
748
|
+
return 1;
|
|
749
|
+
} finally { stopSpinner(spinnerId); }
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
export async function deleteTask(shortId, taskId, opts = {}) {
|
|
753
|
+
const apiKey = await getApiKey();
|
|
754
|
+
if (!apiKey) { console.error(NO_KEY_MESSAGE.trim()); return 1; }
|
|
755
|
+
if (!shortId || !taskId) { process.stderr.write('ERROR: short_id and task_id are required.\n'); return 1; }
|
|
756
|
+
|
|
757
|
+
const apiBase = await getApiBase();
|
|
758
|
+
const timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
759
|
+
const spinnerId = startSpinner('Deleting task');
|
|
760
|
+
|
|
761
|
+
try {
|
|
762
|
+
await apiRequest('DELETE', `/livedocs/${shortId}/tasks/${taskId}`, null, apiKey, apiBase, timeoutMs);
|
|
763
|
+
if (opts.json) { console.log(JSON.stringify({ status: 'ok' }, null, 2)); return 0; }
|
|
764
|
+
process.stdout.write(`Task \`${taskId}\` deleted.\n`);
|
|
765
|
+
return 0;
|
|
766
|
+
} catch (err) {
|
|
767
|
+
process.stderr.write(`Failed to delete task: ${err?.message || err}\n`);
|
|
768
|
+
return 1;
|
|
769
|
+
} finally { stopSpinner(spinnerId); }
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
export async function listTaskRecords(shortId, taskId, opts = {}) {
|
|
773
|
+
const apiKey = await getApiKey();
|
|
774
|
+
if (!apiKey) { console.error(NO_KEY_MESSAGE.trim()); return 1; }
|
|
775
|
+
if (!shortId || !taskId) { process.stderr.write('ERROR: short_id and task_id are required.\n'); return 1; }
|
|
776
|
+
|
|
777
|
+
const apiBase = await getApiBase();
|
|
778
|
+
const timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
779
|
+
const spinnerId = startSpinner('Fetching task records');
|
|
780
|
+
|
|
781
|
+
try {
|
|
782
|
+
const params = new URLSearchParams();
|
|
783
|
+
if (opts.recordType) params.set('record_type', opts.recordType);
|
|
784
|
+
if (opts.page) params.set('page', opts.page);
|
|
785
|
+
if (opts.size) params.set('size', opts.size);
|
|
786
|
+
const qs = params.toString();
|
|
787
|
+
const payload = await apiRequest('GET', `/livedocs/${shortId}/tasks/${taskId}/records${qs ? `?${qs}` : ''}`, null, apiKey, apiBase, timeoutMs);
|
|
788
|
+
if (opts.json) { console.log(JSON.stringify(payload, null, 2)); return 0; }
|
|
789
|
+
const items = payload?.data?.items || [];
|
|
790
|
+
if (!items.length) { process.stderr.write('No records found.\n'); return 0; }
|
|
791
|
+
process.stdout.write(`Found ${payload.data.total || items.length} record(s)\n\n`);
|
|
792
|
+
for (const r of items) process.stdout.write(formatTaskRecord(r));
|
|
793
|
+
return 0;
|
|
794
|
+
} catch (err) {
|
|
795
|
+
process.stderr.write(`Failed to list task records: ${err?.message || err}\n`);
|
|
796
|
+
return 1;
|
|
797
|
+
} finally { stopSpinner(spinnerId); }
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
export async function createTaskComment(shortId, taskId, opts = {}) {
|
|
801
|
+
const apiKey = await getApiKey();
|
|
802
|
+
if (!apiKey) { console.error(NO_KEY_MESSAGE.trim()); return 1; }
|
|
803
|
+
if (!shortId || !taskId) { process.stderr.write('ERROR: short_id and task_id are required.\n'); return 1; }
|
|
804
|
+
if (!opts.content) { process.stderr.write('ERROR: --content is required.\n'); return 1; }
|
|
805
|
+
|
|
806
|
+
const apiBase = await getApiBase();
|
|
807
|
+
const timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
808
|
+
const spinnerId = startSpinner('Adding comment');
|
|
809
|
+
|
|
810
|
+
try {
|
|
811
|
+
const payload = await apiRequest('POST', `/livedocs/${shortId}/tasks/${taskId}/comments`, { content: opts.content }, apiKey, apiBase, timeoutMs);
|
|
812
|
+
if (opts.json) { console.log(JSON.stringify(payload, null, 2)); return 0; }
|
|
813
|
+
process.stdout.write('Comment added.\n');
|
|
814
|
+
process.stdout.write(formatTaskRecord(payload?.data));
|
|
815
|
+
return 0;
|
|
816
|
+
} catch (err) {
|
|
817
|
+
process.stderr.write(`Failed to add comment: ${err?.message || err}\n`);
|
|
818
|
+
return 1;
|
|
819
|
+
} finally { stopSpinner(spinnerId); }
|
|
820
|
+
}
|
|
821
|
+
|
|
534
822
|
export async function pptRetrieve(shortId, opts = {}) {
|
|
535
823
|
const apiKey = await getApiKey();
|
|
536
824
|
if (!apiKey) { console.error(NO_KEY_MESSAGE.trim()); return 1; }
|
package/src/slides.js
CHANGED
|
@@ -48,9 +48,18 @@ function normalizeTaskStatus(status) {
|
|
|
48
48
|
/**
|
|
49
49
|
* Create a PPT task. Returns { task_id, livedoc_short_id, ppt_business_id } or throws.
|
|
50
50
|
* Uses fetchWithTimeoutAndRetry for 5xx retry (per PPT Task API error codes).
|
|
51
|
+
* @param {string} apiKey
|
|
52
|
+
* @param {string} query
|
|
53
|
+
* @param {number} timeoutMs
|
|
54
|
+
* @param {string} apiBase
|
|
55
|
+
* @param {{ ai_theme_id?: string }} [pptConfig]
|
|
51
56
|
*/
|
|
52
|
-
async function createPptTask(apiKey, query, timeoutMs, apiBase) {
|
|
57
|
+
async function createPptTask(apiKey, query, timeoutMs, apiBase, pptConfig) {
|
|
53
58
|
const url = `${apiBase}/v2/ppts`;
|
|
59
|
+
const body = { query: query.trim() };
|
|
60
|
+
if (pptConfig && Object.keys(pptConfig).length > 0) {
|
|
61
|
+
body.ppt_config = pptConfig;
|
|
62
|
+
}
|
|
54
63
|
const res = await fetchWithTimeoutAndRetry(
|
|
55
64
|
url,
|
|
56
65
|
{
|
|
@@ -60,7 +69,7 @@ async function createPptTask(apiKey, query, timeoutMs, apiBase) {
|
|
|
60
69
|
Authorization: `Bearer ${apiKey}`,
|
|
61
70
|
"Content-Type": "application/json",
|
|
62
71
|
},
|
|
63
|
-
body: JSON.stringify(
|
|
72
|
+
body: JSON.stringify(body),
|
|
64
73
|
},
|
|
65
74
|
timeoutMs
|
|
66
75
|
);
|
|
@@ -150,18 +159,30 @@ export async function slides(query, options = {}) {
|
|
|
150
159
|
try {
|
|
151
160
|
const apiBase = await getApiBase();
|
|
152
161
|
|
|
153
|
-
|
|
162
|
+
let createResult = {};
|
|
163
|
+
let taskId;
|
|
154
164
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
165
|
+
if (options.taskId) {
|
|
166
|
+
// Resume polling an existing task
|
|
167
|
+
taskId = options.taskId;
|
|
168
|
+
if (options.verbose || options.json) {
|
|
169
|
+
process.stderr.write(`Resuming task: ${taskId}\n`);
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
process.stderr.write("Creating PPT task...\n");
|
|
173
|
+
|
|
174
|
+
createResult = await createPptTask(
|
|
175
|
+
apiKey,
|
|
176
|
+
query,
|
|
177
|
+
requestTimeoutMs,
|
|
178
|
+
apiBase,
|
|
179
|
+
options.pptConfig
|
|
180
|
+
);
|
|
181
|
+
taskId = createResult.task_id;
|
|
162
182
|
|
|
163
|
-
|
|
164
|
-
|
|
183
|
+
if (options.json && options.verbose) {
|
|
184
|
+
process.stderr.write(`Task ID: ${taskId}\n`);
|
|
185
|
+
}
|
|
165
186
|
}
|
|
166
187
|
|
|
167
188
|
// 默认显示 spinner 动画;仅在使用 -v/--json 时改为逐行状态输出
|
|
@@ -330,3 +351,79 @@ export async function slides(query, options = {}) {
|
|
|
330
351
|
return 1;
|
|
331
352
|
}
|
|
332
353
|
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* List available PPT themes. Returns exit code (0 success, 1 failure).
|
|
357
|
+
* @param {Object} options - { lang?, type?, keyword?, page?, size?, json?, timeoutMs? }
|
|
358
|
+
*/
|
|
359
|
+
export async function listPptThemes(options = {}) {
|
|
360
|
+
const apiKey = await getApiKey();
|
|
361
|
+
if (!apiKey) {
|
|
362
|
+
console.error(NO_KEY_MESSAGE.trim());
|
|
363
|
+
return 1;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
const apiBase = await getApiBase();
|
|
370
|
+
const params = new URLSearchParams();
|
|
371
|
+
if (options.lang) params.set("lang", options.lang);
|
|
372
|
+
if (options.type) params.set("type", options.type);
|
|
373
|
+
if (options.keyword) params.set("keyword", options.keyword);
|
|
374
|
+
if (options.page) params.set("page", String(options.page));
|
|
375
|
+
if (options.size) params.set("size", String(options.size));
|
|
376
|
+
|
|
377
|
+
const qs = params.toString();
|
|
378
|
+
const url = `${apiBase}/v2/ppt-themes${qs ? `?${qs}` : ""}`;
|
|
379
|
+
|
|
380
|
+
const res = await fetchWithTimeoutAndRetry(
|
|
381
|
+
url,
|
|
382
|
+
{
|
|
383
|
+
method: "GET",
|
|
384
|
+
headers: {
|
|
385
|
+
Accept: "application/json",
|
|
386
|
+
Authorization: `Bearer ${apiKey}`,
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
timeoutMs
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
const data = await res.json().catch(() => ({}));
|
|
393
|
+
|
|
394
|
+
if (data.status === "error") {
|
|
395
|
+
const msg = data.message || data.code || "Unknown error";
|
|
396
|
+
throw new Error(msg);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (!res.ok) {
|
|
400
|
+
const msg =
|
|
401
|
+
data.message || data.error || res.statusText || `HTTP ${res.status}`;
|
|
402
|
+
throw new Error(msg);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const themes = data.data ?? [];
|
|
406
|
+
|
|
407
|
+
if (options.json) {
|
|
408
|
+
console.log(JSON.stringify(data, null, 2));
|
|
409
|
+
return 0;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (!Array.isArray(themes) || themes.length === 0) {
|
|
413
|
+
console.log("No themes found.");
|
|
414
|
+
return 0;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
for (const t of themes) {
|
|
418
|
+
console.log(`${t.id} ${t.title || "(untitled)"}`);
|
|
419
|
+
if (t.subtitle) console.log(` subtitle: ${t.subtitle}`);
|
|
420
|
+
if (t.description) console.log(` ${t.description}`);
|
|
421
|
+
console.log();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return 0;
|
|
425
|
+
} catch (err) {
|
|
426
|
+
console.error("Error:", err.message || err);
|
|
427
|
+
return 1;
|
|
428
|
+
}
|
|
429
|
+
}
|
package/src/webFetch.js
CHANGED
|
@@ -8,6 +8,7 @@ const SPINNER_INTERVAL_MS = 80;
|
|
|
8
8
|
const STATUS_PAD = 56;
|
|
9
9
|
|
|
10
10
|
function startSpinner(message) {
|
|
11
|
+
if (!process.stderr.isTTY) return null;
|
|
11
12
|
const start = Date.now();
|
|
12
13
|
let i = 0;
|
|
13
14
|
const id = setInterval(() => {
|
|
@@ -21,7 +22,7 @@ function startSpinner(message) {
|
|
|
21
22
|
|
|
22
23
|
function stopSpinner(id) {
|
|
23
24
|
if (id != null) clearInterval(id);
|
|
24
|
-
process.stderr.write(`\r${' '.repeat(STATUS_PAD)}\r`);
|
|
25
|
+
if (process.stderr.isTTY) process.stderr.write(`\r${' '.repeat(STATUS_PAD)}\r`);
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
async function getApiBase() {
|
package/src/xSearch.js
CHANGED
|
@@ -8,6 +8,7 @@ const SPINNER_INTERVAL_MS = 80;
|
|
|
8
8
|
const STATUS_PAD = 56;
|
|
9
9
|
|
|
10
10
|
function startSpinner(message) {
|
|
11
|
+
if (!process.stderr.isTTY) return null;
|
|
11
12
|
const start = Date.now();
|
|
12
13
|
let i = 0;
|
|
13
14
|
const id = setInterval(() => {
|
|
@@ -21,7 +22,7 @@ function startSpinner(message) {
|
|
|
21
22
|
|
|
22
23
|
function stopSpinner(id) {
|
|
23
24
|
if (id != null) clearInterval(id);
|
|
24
|
-
process.stderr.write(`\r${' '.repeat(STATUS_PAD)}\r`);
|
|
25
|
+
if (process.stderr.isTTY) process.stderr.write(`\r${' '.repeat(STATUS_PAD)}\r`);
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
async function getApiBase() {
|
package/src/youtubeSubtitling.js
CHANGED
|
@@ -8,6 +8,7 @@ const SPINNER_INTERVAL_MS = 80;
|
|
|
8
8
|
const STATUS_PAD = 52;
|
|
9
9
|
|
|
10
10
|
function startSpinner(message) {
|
|
11
|
+
if (!process.stderr.isTTY) return null;
|
|
11
12
|
const start = Date.now();
|
|
12
13
|
let i = 0;
|
|
13
14
|
const id = setInterval(() => {
|
|
@@ -21,7 +22,7 @@ function startSpinner(message) {
|
|
|
21
22
|
|
|
22
23
|
function stopSpinner(id) {
|
|
23
24
|
if (id != null) clearInterval(id);
|
|
24
|
-
process.stderr.write(`\r${' '.repeat(STATUS_PAD)}\r`);
|
|
25
|
+
if (process.stderr.isTTY) process.stderr.write(`\r${' '.repeat(STATUS_PAD)}\r`);
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
/** Extract video ID from a YouTube URL or return the string if it looks like a plain ID. Returns null if invalid. */
|