felo-ai 0.2.3 → 0.2.5

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/README.en.md CHANGED
@@ -21,12 +21,9 @@ Search the web for up-to-date information and get AI-synthesized answers. Ideal
21
21
  - **Claude Code**: After installing the skill, it triggers automatically, or type `/felo-ai your question`
22
22
  - **Examples**: `felo search "Tokyo weather"`, `felo search "React 19 new features" --verbose`
23
23
 
24
- ### Capability 2: Generate PPT
24
+ ### Capability 2: Generate PPT (Felo Slides)
25
25
 
26
- Describe a topic in one sentence and Felo generates a slideshow. The job runs in the cloud; when done, you get an **online document link** to open in your browser.
27
-
28
- - **Terminal**: `felo slides "your topic or description"`
29
- - **Examples**: `felo slides "Felo product intro, 3 slides"`, `felo slides "Introduction to React" --poll-timeout 300`
26
+ Terminal: `felo slides "your topic"`. In Claude Code: install `npx @claude/skills add felo-slides`, then `/felo-slides your topic`. You get an online document link when done. Examples: `felo slides "Felo product intro, 3 slides"`, `felo slides "Introduction to React"`.
30
27
 
31
28
  ---
32
29
 
@@ -62,17 +59,39 @@ Get your API key at [felo.ai](https://felo.ai) (Settings → API Keys).
62
59
  | `felo config set FELO_API_KEY <key>` | Save API key |
63
60
  | `felo config get/list/path/unset` | View / list / path / remove config |
64
61
 
62
+ ### Examples
63
+
64
+ **Search**
65
+
66
+ ```bash
67
+ felo search "Tokyo weather"
68
+ felo search "MacBook Air M3 price"
69
+ felo search "React 19 new features" --verbose
70
+ npx felo-ai search "Tokyo weather"
71
+ ```
72
+
73
+ **Slides**
74
+
75
+ ```bash
76
+ felo slides "Felo product intro, 3 slides"
77
+ felo slides "Introduction to React"
78
+ felo slides "Q4 2024 business review, 10 pages" --poll-timeout 300
79
+ npx felo-ai slides "Tokyo travel guide, 5 slides"
80
+ ```
81
+
65
82
  ---
66
83
 
67
- ## Claude Code Skill
84
+ ## Claude Code Skills
68
85
 
69
- Install the skill:
86
+ **Search** — Install and use real-time search:
70
87
 
71
88
  ```bash
72
- npx @claude/skills add felo-ai
89
+ npx @claude/skills add felo-search
73
90
  ```
74
91
 
75
- After setting `FELO_API_KEY`, ask Claude things like “What’s the weather in Tokyo today?” or “React 19 new features and search will trigger automatically. PPT generation is available only via the terminal with `felo slides`.
92
+ After setting `FELO_API_KEY`, ask Claude things like “What’s the weather in Tokyo today?” or “React 19 new features”; the search skill triggers automatically (or use `/felo-search your question`).
93
+
94
+ **Slides (PPT)** — `npx @claude/skills add felo-slides`, then `/felo-slides your topic`. Same `FELO_API_KEY`. [Details →](./felo-slides/README.md)
76
95
 
77
96
  ---
78
97
 
package/README.md CHANGED
@@ -1,30 +1,27 @@
1
- # Felo Skills for Claude Code
1
+ # Felo AI CLI
2
2
 
3
- **Ask anything. Get current answers powered by AI.**
3
+ **Ask anything. Get current answers. Generate slides from a prompt.**
4
4
 
5
- Real-time web search powered by Felo AI. Works in Chinese, English, Japanese, and Korean.
5
+ [npm package: **felo-ai**](https://www.npmjs.com/package/felo-ai) — Real-time search and PPT generation from the terminal. Also works as Claude Code skills. Supports Chinese, English, Japanese, and Korean.
6
6
 
7
- [![Setup Time](https://img.shields.io/badge/setup-2%20minutes-blue)]() [![License](https://img.shields.io/badge/license-MIT-green)]()
7
+ [![npm version](https://img.shields.io/npm/v/felo-ai.svg)](https://www.npmjs.com/package/felo-ai) [![License](https://img.shields.io/badge/license-MIT-green)]()
8
8
 
9
9
  ---
10
10
 
11
- ## Felo CLI (Terminal)
12
-
13
- Use Felo search from the terminal without opening Claude Code.
14
-
15
- ### Install
11
+ ## Install (CLI)
16
12
 
17
13
  ```bash
18
- npm install -g felo-search
14
+ npm install -g felo-ai
19
15
  ```
20
16
 
21
- Or run without installing (uses latest published version):
17
+ Run without installing:
22
18
 
23
19
  ```bash
24
- npx felo-search search "Tokyo weather"
20
+ npx felo-ai search "Tokyo weather"
21
+ npx felo-ai slides "Introduction to React, 5 slides"
25
22
  ```
26
23
 
27
- After install, the command is `felo` (from the package name `felo-search`).
24
+ After install, the command is `felo` (package name: **felo-ai**).
28
25
 
29
26
  ### Configure API key
30
27
 
@@ -53,32 +50,47 @@ Get your API key from [felo.ai](https://felo.ai) (Settings → API Keys). Enviro
53
50
  | Command | Description |
54
51
  | ------------------------------------ | ----------------------------------------------------- |
55
52
  | `felo search "<query>"` | Search for current info (weather, news, prices, etc.) |
53
+ | `felo slides "<prompt>"` | Generate PPT; returns link when done |
56
54
  | `felo config set FELO_API_KEY <key>` | Save API key to config |
57
55
  | `felo config get FELO_API_KEY` | Print stored key |
58
56
  | `felo config list` | List config keys |
59
57
  | `felo config path` | Show config file path |
60
- | `felo summarize` / `felo translate` | Planned; use `felo search` for now |
61
58
 
62
59
  ### Examples
63
60
 
61
+ **Search**
62
+
64
63
  ```bash
65
64
  felo search "Tokyo weather"
66
65
  felo search "MacBook Air M3 price"
67
66
  felo search "React 19 new features" --verbose
68
67
  felo search "Hangzhou tomorrow weather" --json
69
- npx felo-search search "Tokyo weather"
68
+ npx felo-ai search "Tokyo weather"
69
+ ```
70
+
71
+ **Slides**
72
+
73
+ ```bash
74
+ felo slides "Felo product intro, 3 slides"
75
+ felo slides "Introduction to React"
76
+ felo slides "Q4 2024 business review, 10 pages" --poll-timeout 300
77
+ npx felo-ai slides "Tokyo travel guide, 5 slides"
70
78
  ```
71
79
 
72
80
  ### CLI FAQ
73
81
 
74
82
  - **Key not found?** Run `felo config set FELO_API_KEY <key>` or set the `FELO_API_KEY` environment variable.
75
83
  - **Request timeout?** Use `felo search "query" --timeout 120` (default 60 seconds). 5xx errors are retried automatically with backoff.
84
+ - **Slides taking long?** Use `felo slides "topic" --poll-timeout 300` (default 1200s) to limit wait.
76
85
  - **Where is config stored?** Run `felo config path` to see the file (e.g. `~/.felo/config.json`).
77
- - **Streaming?** Not yet; when the Felo API supports streaming, the CLI can be updated to stream output.
78
86
 
79
87
  ---
80
88
 
81
- ## Quick Start (Claude Code Skill)
89
+ ## Claude Code Skills (optional)
90
+
91
+ This repo also provides **Claude Code** skills. If you use [Claude Code](https://claude.ai/code), you can install search and/or slides as skills so Claude can run them in chat.
92
+
93
+ ### Quick Start (Search skill)
82
94
 
83
95
  Install the skill:
84
96
 
@@ -110,6 +122,8 @@ Ask Claude: "What's the weather in Tokyo today?"
110
122
 
111
123
  **You're done!** The skill triggers automatically for any question needing current information.
112
124
 
125
+ **Felo Slides (PPT):** In terminal run `felo slides "your topic"`. In Claude Code install with `npx @claude/skills add felo-slides`, then use `/felo-slides your topic`. See [felo-slides](./felo-slides/README.md).
126
+
113
127
  ---
114
128
 
115
129
  ## Usage Examples
@@ -284,6 +298,10 @@ Real-time web search with AI-generated answers.
284
298
 
285
299
  **[View skill documentation →](./felo-search/)**
286
300
 
301
+ ### felo-slides
302
+
303
+ Generate PPT: in terminal use `felo slides "your topic"`, in Claude Code use `/felo-slides your topic`. **[View skill documentation →](./felo-slides/)**
304
+
287
305
  ---
288
306
 
289
307
  ## Contributing
@@ -302,6 +320,7 @@ Run CLI tests: `npm test`
302
320
 
303
321
  ## Links
304
322
 
323
+ - **[npm: felo-ai](https://www.npmjs.com/package/felo-ai)** — CLI package
305
324
  - **[Felo Open Platform](https://openapi.felo.ai/docs/)** — Get your API key
306
325
  - **[API Documentation](https://openapi.felo.ai/docs/api-reference/v2/chat.html)** — API reference
307
326
  - **[Claude Code](https://claude.ai/code)** — AI assistant CLI
@@ -321,7 +340,7 @@ Run CLI tests: `npm test`
321
340
 
322
341
  ## License
323
342
 
324
- MIT License — see [LICENSE](./felo-search/LICENSE) for details.
343
+ MIT — see [LICENSE](./felo-search/LICENSE) in the repo for details.
325
344
 
326
345
  ---
327
346
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "felo-ai",
3
- "version": "0.2.3",
4
- "description": "Felo AI CLI - real-time search from the terminal",
3
+ "version": "0.2.5",
4
+ "description": "Felo AI CLI - real-time search and PPT generation from the terminal",
5
5
  "type": "module",
6
6
  "main": "src/cli.js",
7
7
  "bin": {
package/src/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  import { createRequire } from 'module';
4
4
  import { Command } from 'commander';
package/src/slides.js CHANGED
@@ -1,22 +1,26 @@
1
- import { getApiKey, fetchWithTimeoutAndRetry, NO_KEY_MESSAGE } from './search.js';
1
+ import {
2
+ getApiKey,
3
+ fetchWithTimeoutAndRetry,
4
+ NO_KEY_MESSAGE,
5
+ } from "./search.js";
2
6
 
3
- const DEFAULT_API_BASE = 'https://openapi.felo.ai';
7
+ const DEFAULT_API_BASE = "https://openapi.felo.ai";
4
8
  const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
5
9
  const POLL_INTERVAL_MS = 10_000;
6
10
  const MAX_POLL_TIMEOUT_MS = 1_200_000; // 20 minutes max wait
7
11
 
8
- const SPINNER_FRAMES = ['', '', '', '', '', '', '', '', '', ''];
12
+ const SPINNER_FRAMES = ["", "", "", "", "", "", "", "", "", ""];
9
13
  const STATUS_LINE_PAD = 50;
10
14
 
11
15
  /** API base URL (default https://openapi.felo.ai). Override via FELO_API_BASE env or config if needed. */
12
16
  async function getApiBase() {
13
17
  let base = process.env.FELO_API_BASE?.trim();
14
18
  if (!base) {
15
- const { getConfigValue } = await import('./config.js');
16
- const v = await getConfigValue('FELO_API_BASE');
17
- base = typeof v === 'string' ? v.trim() : '';
19
+ const { getConfigValue } = await import("./config.js");
20
+ const v = await getConfigValue("FELO_API_BASE");
21
+ base = typeof v === "string" ? v.trim() : "";
18
22
  }
19
- const normalized = (base || DEFAULT_API_BASE).replace(/\/$/, '');
23
+ const normalized = (base || DEFAULT_API_BASE).replace(/\/$/, "");
20
24
  return normalized;
21
25
  }
22
26
 
@@ -24,19 +28,21 @@ function sleep(ms) {
24
28
  return new Promise((resolve) => setTimeout(resolve, ms));
25
29
  }
26
30
 
27
- /** Write a single overwritable status line: spinner + elapsed. Call clearStatusLine() before next stdout. */
28
- function writeStatusLine(spinnerFrame, elapsedSec) {
31
+ /** Write status line: spinner + elapsed. When overwrite is true use \\r (TTY); else use \\n so non-TTY still shows animation. */
32
+ function writeStatusLine(spinnerFrame, elapsedSec, overwrite = true) {
29
33
  const s = `Generating slides... ${spinnerFrame} ${elapsedSec}s`;
30
- const padded = s.padEnd(STATUS_LINE_PAD, ' ');
31
- process.stderr.write(`\r${padded}`);
34
+ const padded = s.padEnd(STATUS_LINE_PAD, " ");
35
+ process.stderr.write(overwrite ? `\r${padded}` : `${padded}\n`);
32
36
  }
33
37
 
34
38
  function clearStatusLine() {
35
- process.stderr.write(`\r${' '.repeat(STATUS_LINE_PAD)}\r`);
39
+ process.stderr.write(`\r${" ".repeat(STATUS_LINE_PAD)}\r`);
36
40
  }
37
41
 
38
42
  function normalizeTaskStatus(status) {
39
- return String(status || '').trim().toUpperCase();
43
+ return String(status || "")
44
+ .trim()
45
+ .toUpperCase();
40
46
  }
41
47
 
42
48
  /**
@@ -48,11 +54,11 @@ async function createPptTask(apiKey, query, timeoutMs, apiBase) {
48
54
  const res = await fetchWithTimeoutAndRetry(
49
55
  url,
50
56
  {
51
- method: 'POST',
57
+ method: "POST",
52
58
  headers: {
53
- 'Accept': 'application/json',
54
- 'Authorization': `Bearer ${apiKey}`,
55
- 'Content-Type': 'application/json',
59
+ Accept: "application/json",
60
+ Authorization: `Bearer ${apiKey}`,
61
+ "Content-Type": "application/json",
56
62
  },
57
63
  body: JSON.stringify({ query: query.trim() }),
58
64
  },
@@ -61,19 +67,20 @@ async function createPptTask(apiKey, query, timeoutMs, apiBase) {
61
67
 
62
68
  const data = await res.json().catch(() => ({}));
63
69
 
64
- if (data.status === 'error') {
65
- const msg = data.message || data.code || 'Unknown error';
70
+ if (data.status === "error") {
71
+ const msg = data.message || data.code || "Unknown error";
66
72
  throw new Error(msg);
67
73
  }
68
74
 
69
75
  if (!res.ok) {
70
- const msg = data.message || data.error || res.statusText || `HTTP ${res.status}`;
76
+ const msg =
77
+ data.message || data.error || res.statusText || `HTTP ${res.status}`;
71
78
  throw new Error(msg);
72
79
  }
73
80
 
74
81
  const payload = data.data;
75
82
  if (!payload || !payload.task_id) {
76
- throw new Error('Unexpected response: missing task_id');
83
+ throw new Error("Unexpected response: missing task_id");
77
84
  }
78
85
 
79
86
  return payload;
@@ -88,10 +95,10 @@ async function getTaskHistorical(apiKey, taskId, timeoutMs, apiBase) {
88
95
  const res = await fetchWithTimeoutAndRetry(
89
96
  url,
90
97
  {
91
- method: 'GET',
98
+ method: "GET",
92
99
  headers: {
93
- 'Accept': 'application/json',
94
- 'Authorization': `Bearer ${apiKey}`,
100
+ Accept: "application/json",
101
+ Authorization: `Bearer ${apiKey}`,
95
102
  },
96
103
  },
97
104
  timeoutMs
@@ -99,19 +106,20 @@ async function getTaskHistorical(apiKey, taskId, timeoutMs, apiBase) {
99
106
 
100
107
  const data = await res.json().catch(() => ({}));
101
108
 
102
- if (data.status === 'error') {
103
- const msg = data.message || data.code || 'Unknown error';
109
+ if (data.status === "error") {
110
+ const msg = data.message || data.code || "Unknown error";
104
111
  throw new Error(msg);
105
112
  }
106
113
 
107
114
  if (!res.ok) {
108
- const msg = data.message || data.error || res.statusText || `HTTP ${res.status}`;
115
+ const msg =
116
+ data.message || data.error || res.statusText || `HTTP ${res.status}`;
109
117
  throw new Error(msg);
110
118
  }
111
119
 
112
120
  const payload = data.data;
113
121
  if (!payload) {
114
- throw new Error('Unexpected response: missing data');
122
+ throw new Error("Unexpected response: missing data");
115
123
  }
116
124
 
117
125
  return {
@@ -120,6 +128,7 @@ async function getTaskHistorical(apiKey, taskId, timeoutMs, apiBase) {
120
128
  ppt_biz_id: payload.ppt_biz_id,
121
129
  live_doc_url: payload.live_doc_url,
122
130
  live_doc_short_id: payload.live_doc_short_id ?? payload.livedoc_short_id,
131
+ error_message: payload.error_message,
123
132
  };
124
133
  }
125
134
 
@@ -141,64 +150,85 @@ export async function slides(query, options = {}) {
141
150
  try {
142
151
  const apiBase = await getApiBase();
143
152
 
144
- process.stderr.write('Creating PPT task...\n');
153
+ process.stderr.write("Creating PPT task...\n");
145
154
 
146
- const createResult = await createPptTask(apiKey, query, requestTimeoutMs, apiBase);
155
+ const createResult = await createPptTask(
156
+ apiKey,
157
+ query,
158
+ requestTimeoutMs,
159
+ apiBase
160
+ );
147
161
  const taskId = createResult.task_id;
148
162
 
149
163
  if (options.json && options.verbose) {
150
164
  process.stderr.write(`Task ID: ${taskId}\n`);
151
165
  }
152
166
 
153
- const useLiveStatus =
154
- process.stderr.isTTY && !options.verbose && !options.json;
167
+ // 默认显示 spinner 动画;仅在使用 -v/--json 时改为逐行状态输出
168
+ const useLiveStatus = !options.verbose && !options.json;
169
+ const canOverwriteLine = process.stderr.isTTY && useLiveStatus;
155
170
  if (!useLiveStatus) {
156
- process.stderr.write('Generating slides (this may take a minute)...\n');
171
+ process.stderr.write("Generating slides (this may take a minute)...\n");
157
172
  }
158
173
 
159
174
  const startTime = Date.now();
160
175
  let lastStatus;
161
176
  let spinIndex = 0;
162
- if (useLiveStatus) writeStatusLine(SPINNER_FRAMES[0], 0);
177
+ if (useLiveStatus) writeStatusLine(SPINNER_FRAMES[0], 0, canOverwriteLine);
163
178
 
164
179
  while (Date.now() - startTime < pollTimeoutMs) {
165
180
  await sleep(pollIntervalMs);
166
181
 
167
- const historical = await getTaskHistorical(apiKey, taskId, requestTimeoutMs, apiBase);
182
+ const historical = await getTaskHistorical(
183
+ apiKey,
184
+ taskId,
185
+ requestTimeoutMs,
186
+ apiBase
187
+ );
168
188
  const normalizedStatus = normalizeTaskStatus(historical.task_status);
169
189
  lastStatus = normalizedStatus || historical.task_status;
170
190
 
171
191
  const elapsed = Math.floor((Date.now() - startTime) / 1000);
172
192
  if (useLiveStatus) {
173
193
  spinIndex = (spinIndex + 1) % SPINNER_FRAMES.length;
174
- writeStatusLine(SPINNER_FRAMES[spinIndex], elapsed);
194
+ writeStatusLine(SPINNER_FRAMES[spinIndex], elapsed, canOverwriteLine);
175
195
  } else if (!options.verbose && !options.json) {
176
196
  process.stderr.write(` Generating... ${elapsed}s\n`);
177
197
  }
178
198
 
179
- const done = normalizedStatus === 'COMPLETED' || normalizedStatus === 'SUCCESS';
199
+ const done =
200
+ normalizedStatus === "COMPLETED" || normalizedStatus === "SUCCESS";
180
201
  if (done) {
181
- if (useLiveStatus) clearStatusLine();
202
+ if (useLiveStatus && canOverwriteLine) clearStatusLine();
182
203
  const pptUrl =
183
204
  historical.ppt_url ||
184
- (historical.ppt_biz_id ? `https://dev.felo.ai/slides/${historical.ppt_biz_id}` : null);
205
+ (historical.ppt_biz_id
206
+ ? `https://dev.felo.ai/slides/${historical.ppt_biz_id}`
207
+ : null);
185
208
  const liveDocUrl =
186
209
  historical.live_doc_url ||
187
- (historical.live_doc_short_id ? `https://felo.ai/livedoc/${historical.live_doc_short_id}` : null) ||
188
- (createResult.livedoc_short_id ? `https://felo.ai/livedoc/${createResult.livedoc_short_id}` : null);
210
+ (historical.live_doc_short_id
211
+ ? `https://felo.ai/livedoc/${historical.live_doc_short_id}`
212
+ : null) ||
213
+ (createResult.livedoc_short_id
214
+ ? `https://felo.ai/livedoc/${createResult.livedoc_short_id}`
215
+ : null);
189
216
  const url = pptUrl || liveDocUrl;
190
217
  if (options.json) {
191
218
  console.log(
192
219
  JSON.stringify(
193
220
  {
194
- status: 'ok',
221
+ status: "ok",
195
222
  data: {
196
223
  task_id: taskId,
197
224
  task_status: normalizedStatus || historical.task_status,
198
225
  ppt_url: pptUrl,
199
- ppt_biz_id: historical.ppt_biz_id ?? createResult.ppt_business_id,
226
+ ppt_biz_id:
227
+ historical.ppt_biz_id ?? createResult.ppt_business_id,
200
228
  live_doc_url: liveDocUrl,
201
- livedoc_short_id: historical.live_doc_short_id ?? createResult.livedoc_short_id,
229
+ livedoc_short_id:
230
+ historical.live_doc_short_id ??
231
+ createResult.livedoc_short_id,
202
232
  ppt_business_id: createResult.ppt_business_id,
203
233
  },
204
234
  },
@@ -209,40 +239,94 @@ export async function slides(query, options = {}) {
209
239
  } else {
210
240
  if (url) {
211
241
  if (pptUrl && !options.json) {
212
- process.stderr.write('PPT ready. Open this link to preview:\n');
242
+ process.stderr.write("PPT ready. Open this link to preview:\n");
213
243
  }
214
244
  console.log(pptUrl || liveDocUrl);
215
245
  } else {
216
246
  if (useLiveStatus) clearStatusLine();
217
- console.error('Error: Completed but no ppt_url or live_doc_url in response');
247
+ console.error(
248
+ "Error: Completed but no ppt_url or live_doc_url in response"
249
+ );
218
250
  return 1;
219
251
  }
220
252
  }
221
253
  return 0;
222
254
  }
223
255
 
224
- if (
225
- normalizedStatus === 'FAILED' ||
226
- normalizedStatus === 'ERROR'
227
- ) {
228
- if (useLiveStatus) clearStatusLine();
229
- console.error(`Error: Task finished with status: ${normalizedStatus || historical.task_status}`);
256
+ const terminalErrorStatuses = [
257
+ "FAILED",
258
+ "PENDING",
259
+ "EXPIRED",
260
+ "CANCELED",
261
+ ];
262
+
263
+ if (terminalErrorStatuses.includes(normalizedStatus)) {
264
+ if (useLiveStatus && canOverwriteLine) clearStatusLine();
265
+
266
+ const liveDocUrl =
267
+ historical.live_doc_url ||
268
+ (historical.live_doc_short_id
269
+ ? `https://felo.ai/livedoc/${historical.live_doc_short_id}`
270
+ : null) ||
271
+ (createResult.livedoc_short_id
272
+ ? `https://felo.ai/livedoc/${createResult.livedoc_short_id}`
273
+ : null);
274
+
275
+ const errorMessage =
276
+ historical.error_message ||
277
+ `Task finished with status: ${
278
+ normalizedStatus || historical.task_status
279
+ }`;
280
+
281
+ if (options.json) {
282
+ console.log(
283
+ JSON.stringify(
284
+ {
285
+ status: "error",
286
+ data: {
287
+ task_id: taskId,
288
+ task_status: normalizedStatus || historical.task_status,
289
+ live_doc_url: liveDocUrl,
290
+ livedoc_short_id:
291
+ historical.live_doc_short_id ??
292
+ createResult.livedoc_short_id,
293
+ error_message: errorMessage,
294
+ },
295
+ },
296
+ null,
297
+ 2
298
+ )
299
+ );
300
+ } else {
301
+ if (liveDocUrl) {
302
+ console.log(liveDocUrl);
303
+ }
304
+ console.error(errorMessage);
305
+ }
306
+
230
307
  return 1;
231
308
  }
232
309
 
233
310
  if (options.verbose) {
234
- process.stderr.write(` Status: ${normalizedStatus || historical.task_status || 'UNKNOWN'}\n`);
311
+ process.stderr.write(
312
+ ` Status: ${
313
+ normalizedStatus || historical.task_status || "UNKNOWN"
314
+ }\n`
315
+ );
235
316
  }
236
317
  }
237
318
 
238
- if (useLiveStatus) clearStatusLine();
319
+ if (useLiveStatus && canOverwriteLine) clearStatusLine();
239
320
  console.error(
240
- `Error: Timed out after ${pollTimeoutMs / 1000}s. Last status: ${lastStatus ?? 'unknown'}`
321
+ `Error: Timed out after ${pollTimeoutMs / 1000}s. Last status: ${
322
+ lastStatus ?? "unknown"
323
+ }`
241
324
  );
242
325
  return 1;
243
326
  } catch (err) {
244
- if (process.stderr.isTTY && !options.verbose && !options.json) clearStatusLine();
245
- console.error('Error:', err.message || err);
327
+ if (process.stderr.isTTY && !options.verbose && !options.json)
328
+ clearStatusLine();
329
+ console.error("Error:", err.message || err);
246
330
  return 1;
247
331
  }
248
332
  }