felo-ai 0.2.4 → 0.2.6

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/cli.js CHANGED
@@ -1,13 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { createRequire } from 'module';
4
- import { Command } from 'commander';
5
- import { search } from './search.js';
6
- import { slides } from './slides.js';
7
- import * as config from './config.js';
3
+ import { createRequire } from "module";
4
+ import { Command } from "commander";
5
+ import { search } from "./search.js";
6
+ import { slides } from "./slides.js";
7
+ import { webExtract } from "./webExtract.js";
8
+ import { youtubeSubtitling } from "./youtubeSubtitling.js";
9
+ import * as config from "./config.js";
8
10
 
9
11
  const require = createRequire(import.meta.url);
10
- const pkg = require('../package.json');
12
+ const pkg = require("../package.json");
11
13
 
12
14
  /** Delay (ms) before process.exit to let Windows libuv finish handle cleanup. */
13
15
  const EXIT_DELAY_MS = 50;
@@ -22,13 +24,13 @@ function flushStdioThenExit(code) {
22
24
  const doExit = () => setTimeout(() => process.exit(code), EXIT_DELAY_MS);
23
25
  const flushStderr = () => {
24
26
  if (process.stderr?.writable && !process.stderr.destroyed) {
25
- process.stderr.write('', () => doExit());
27
+ process.stderr.write("", () => doExit());
26
28
  } else {
27
29
  doExit();
28
30
  }
29
31
  };
30
32
  if (process.stdout?.writable && !process.stdout.destroyed) {
31
- process.stdout.write('', () => flushStderr());
33
+ process.stdout.write("", () => flushStderr());
32
34
  } else {
33
35
  flushStderr();
34
36
  }
@@ -37,17 +39,17 @@ function flushStdioThenExit(code) {
37
39
  const program = new Command();
38
40
 
39
41
  program
40
- .name('felo')
41
- .description('Felo AI CLI - real-time search from the terminal')
42
+ .name("felo")
43
+ .description("Felo AI CLI - real-time search from the terminal")
42
44
  .version(pkg.version);
43
45
 
44
46
  program
45
- .command('search')
46
- .description('Search for current information (weather, news, docs, etc.)')
47
- .argument('<query>', 'search query')
48
- .option('-j, --json', 'output raw JSON')
49
- .option('-v, --verbose', 'show query analysis and sources')
50
- .option('-t, --timeout <seconds>', 'request timeout in seconds', '60')
47
+ .command("search")
48
+ .description("Search for current information (weather, news, docs, etc.)")
49
+ .argument("<query>", "search query")
50
+ .option("-j, --json", "output raw JSON")
51
+ .option("-v, --verbose", "show query analysis and sources")
52
+ .option("-t, --timeout <seconds>", "request timeout in seconds", "60")
51
53
  .action(async (query, opts) => {
52
54
  const timeoutMs = parseInt(opts.timeout, 10) * 1000;
53
55
  const code = await search(query, {
@@ -60,13 +62,26 @@ program
60
62
  });
61
63
 
62
64
  program
63
- .command('slides')
64
- .description('Generate PPT/slides from a prompt (async task, outputs live doc URL when done)')
65
- .argument('<query>', 'PPT generation prompt (e.g. "Felo, 2 pages" or "Introduction to React")')
66
- .option('-j, --json', 'output raw JSON with task_id and live_doc_url')
67
- .option('-v, --verbose', 'show polling status')
68
- .option('-t, --timeout <seconds>', 'request timeout in seconds for each API call', '60')
69
- .option('--poll-timeout <seconds>', 'max seconds to wait for task completion', '1200')
65
+ .command("slides")
66
+ .description(
67
+ "Generate PPT/slides from a prompt (async task, outputs live doc URL when done)"
68
+ )
69
+ .argument(
70
+ "<query>",
71
+ 'PPT generation prompt (e.g. "Felo, 2 pages" or "Introduction to React")'
72
+ )
73
+ .option("-j, --json", "output raw JSON with task_id and live_doc_url")
74
+ .option("-v, --verbose", "show polling status")
75
+ .option(
76
+ "-t, --timeout <seconds>",
77
+ "request timeout in seconds for each API call",
78
+ "60"
79
+ )
80
+ .option(
81
+ "--poll-timeout <seconds>",
82
+ "max seconds to wait for task completion",
83
+ "1200"
84
+ )
70
85
  .action(async (query, opts) => {
71
86
  const timeoutMs = parseInt(opts.timeout, 10) * 1000;
72
87
  const pollTimeoutMs = parseInt(opts.pollTimeout, 10) * 1000 || 1_200_000;
@@ -81,97 +96,156 @@ program
81
96
  });
82
97
 
83
98
  const configCmd = program
84
- .command('config')
85
- .description('Manage persisted config (e.g. FELO_API_KEY). Stored in ~/.felo/config.json');
99
+ .command("config")
100
+ .description(
101
+ "Manage persisted config (e.g. FELO_API_KEY). Stored in ~/.felo/config.json"
102
+ );
86
103
 
87
104
  configCmd
88
- .command('set <key> <value>')
89
- .description('Set a config value (e.g. felo config set FELO_API_KEY your-key)')
105
+ .command("set <key> <value>")
106
+ .description(
107
+ "Set a config value (e.g. felo config set FELO_API_KEY your-key)"
108
+ )
90
109
  .action(async (key, value) => {
91
110
  try {
92
111
  await config.setConfig(key, value);
93
112
  console.log(`Set ${key}`);
94
113
  flushStdioThenExit(0);
95
114
  } catch (e) {
96
- console.error('Error:', e.message);
115
+ console.error("Error:", e.message);
97
116
  flushStdioThenExit(1);
98
117
  }
99
118
  });
100
119
 
101
120
  configCmd
102
- .command('get <key>')
103
- .description('Get a config value (sensitive keys are masked)')
121
+ .command("get <key>")
122
+ .description("Get a config value (sensitive keys are masked)")
104
123
  .action(async (key) => {
105
124
  try {
106
125
  const value = await config.getConfigValue(key);
107
126
  if (value === undefined || value === null) {
108
- console.log('(not set)');
127
+ console.log("(not set)");
109
128
  } else {
110
129
  console.log(config.maskValueForDisplay(key, value));
111
130
  }
112
131
  flushStdioThenExit(0);
113
132
  } catch (e) {
114
- console.error('Error:', e.message);
133
+ console.error("Error:", e.message);
115
134
  flushStdioThenExit(1);
116
135
  }
117
136
  });
118
137
 
119
138
  configCmd
120
- .command('list')
121
- .description('List all config keys (values are hidden)')
139
+ .command("list")
140
+ .description("List all config keys (values are hidden)")
122
141
  .action(async () => {
123
142
  try {
124
143
  const c = await config.listConfig();
125
144
  const keys = Object.keys(c);
126
145
  if (keys.length === 0) {
127
- console.log('No config set. Use: felo config set FELO_API_KEY <key>');
146
+ console.log("No config set. Use: felo config set FELO_API_KEY <key>");
128
147
  } else {
129
148
  keys.forEach((k) => console.log(k));
130
149
  }
131
150
  flushStdioThenExit(0);
132
151
  } catch (e) {
133
- console.error('Error:', e.message);
152
+ console.error("Error:", e.message);
134
153
  flushStdioThenExit(1);
135
154
  }
136
155
  });
137
156
 
138
157
  configCmd
139
- .command('unset <key>')
140
- .description('Remove a config value')
158
+ .command("unset <key>")
159
+ .description("Remove a config value")
141
160
  .action(async (key) => {
142
161
  try {
143
162
  await config.unsetConfig(key);
144
163
  console.log(`Unset ${key}`);
145
164
  flushStdioThenExit(0);
146
165
  } catch (e) {
147
- console.error('Error:', e.message);
166
+ console.error("Error:", e.message);
148
167
  flushStdioThenExit(1);
149
168
  }
150
169
  });
151
170
 
152
171
  configCmd
153
- .command('path')
154
- .description('Show config file path')
172
+ .command("path")
173
+ .description("Show config file path")
155
174
  .action(() => {
156
175
  console.log(config.getConfigPath());
157
176
  flushStdioThenExit(0);
158
177
  });
159
178
 
160
179
  program
161
- .command('summarize')
162
- .description('Summarize text or URL (coming when API is available)')
163
- .argument('[input]', 'text or URL to summarize')
180
+ .command("web-extract")
181
+ .description("Extract webpage content from a URL (markdown, text, or html)")
182
+ .requiredOption("-u, --url <url>", "page URL to extract")
183
+ .option(
184
+ "-f, --format <format>",
185
+ "output format: html, text, markdown",
186
+ "markdown"
187
+ )
188
+ .option(
189
+ "--target-selector <selector>",
190
+ "CSS selector for target element only"
191
+ )
192
+ .option(
193
+ "--wait-for-selector <selector>",
194
+ "wait for selector before extracting"
195
+ )
196
+ .option("--readability", "use readability (main content only)")
197
+ .option("--crawl-mode <mode>", "crawl mode: fast or fine", "fast")
198
+ .option("-t, --timeout <seconds>", "request timeout in seconds", "60")
199
+ .option("-j, --json", "output full API response as JSON")
200
+ .action(async (opts) => {
201
+ const timeoutMs = parseInt(opts.timeout, 10) * 1000;
202
+ const code = await webExtract({
203
+ url: opts.url,
204
+ format: opts.format,
205
+ targetSelector: opts.targetSelector,
206
+ waitForSelector: opts.waitForSelector,
207
+ readability: opts.readability,
208
+ crawlMode: opts.crawlMode,
209
+ timeoutMs: Number.isNaN(timeoutMs) ? 60000 : timeoutMs,
210
+ json: opts.json,
211
+ });
212
+ process.exitCode = code;
213
+ flushStdioThenExit(code);
214
+ });
215
+
216
+ program
217
+ .command("youtube-subtitling")
218
+ .description("Fetch YouTube video subtitles/captions by video URL or ID")
219
+ .requiredOption("-v, --video-code <url-or-id>", "YouTube video URL or video ID (e.g. https://youtube.com/watch?v=ID)")
220
+ .option("-l, --language <code>", "Subtitle language (e.g. en, zh-CN)")
221
+ .option("--with-time", "Include start/duration per segment")
222
+ .option("-j, --json", "Output full API response as JSON")
223
+ .action(async (opts) => {
224
+ const code = await youtubeSubtitling({
225
+ videoCode: opts.videoCode,
226
+ language: opts.language,
227
+ withTime: opts.withTime,
228
+ json: opts.json,
229
+ });
230
+ process.exitCode = code;
231
+ flushStdioThenExit(code);
232
+ });
233
+
234
+ program
235
+ .command("summarize")
236
+ .description("Summarize text or URL (coming when API is available)")
237
+ .argument("[input]", "text or URL to summarize")
164
238
  .action(() => {
165
- console.error('summarize: not yet implemented. Use felo search for now.');
239
+ console.error("summarize: not yet implemented. Use felo search for now.");
166
240
  flushStdioThenExit(1);
167
241
  });
168
242
 
169
243
  program
170
- .command('translate')
171
- .description('Translate text (coming when API is available)')
172
- .argument('[text]', 'text to translate')
244
+ .command("translate")
245
+ .description("Translate text (coming when API is available)")
246
+ .argument("[text]", "text to translate")
173
247
  .action(() => {
174
- console.error('translate: not yet implemented. Use felo search for now.');
248
+ console.error("translate: not yet implemented. Use felo search for now.");
175
249
  flushStdioThenExit(1);
176
250
  });
177
251
 
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
  }