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/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({ query: query.trim() }),
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
- process.stderr.write("Creating PPT task...\n");
162
+ let createResult = {};
163
+ let taskId;
154
164
 
155
- const createResult = await createPptTask(
156
- apiKey,
157
- query,
158
- requestTimeoutMs,
159
- apiBase
160
- );
161
- const taskId = createResult.task_id;
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
- if (options.json && options.verbose) {
164
- process.stderr.write(`Task ID: ${taskId}\n`);
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() {
@@ -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. */